#+PROPERTY: header-args :mkdirp yes #+PROPERTY: header-args:bash :tangle-mode (identity #o755) :comments link :shebang "#!/usr/bin/env bash" #+PROPERTY: header-args:emacs-lisp :tangle ~/.emacs.d/init.el :mkdirp yes :eval never-export :exports both #+TODO: CHECK(s) | OFF(o) #+TITLE: Emacs config #+OPTIONS: broken-links:auto h:6 toc:nil #+begin_quote 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. #+end_quote - Me, <2021-05-27 Thu 17:35> in commit 93a0573. Adapted from [[https://www.youtube.com/watch?v=pIdBinlW40E][The Dark Element - "The Pallbearer Walks Alone"]]. T_T * Introduction My configuration of [[https://www.gnu.org/software/emacs/][GNU Emacs]], an awesome +text editor+ piece of software that can do almost anything. At the moment of writing this, that "almost anything" includes: - *Programming environment*. With LSP & Co, Emacs is as good as many IDEs and is certainly on par with editors like VS Code.\\ Emacs is also particularly great at writing Lisp code, e.g. Clojure, Common Lisp, and of course, Emacs Lisp. - *Org Mode* is useful for a lot of things. My use cases include: - *[[https://leanpub.com/lit-config/read][Literate configuration]]* - *Interactive programming* à la Jupyter Notebook - *Task / project management* - *Formatting documents*. I've written my Master's Thesis in Org Mode. - *Notetaking*, mostly with org-roam and org-journal - *File management*. Dired is my primary file manager. - *Email*, with notmuch. - *Multimedia management*, with EMMS. - *RSS feed reader*, with elfeed. - *Managing passwords*, with pass. - *Messengers*: - *IRC*, with ERC. - *Telegram*, with telega.el - *X Window management*, with EXWM. I literally live in Emacs. - ... As I mentioned above, this document is a piece of literate configuration, i.e. program code interwoven with (occasionally semi-broken) English-language commentary. I find that approach helpful for maintaining the configuration, but the quality and quantity of comments may vary. I also usually incorporate my Emacs-related blog posts back into this config. So, you might extract something of value from here if you're an avid Emacs user, but probably not if you're a newcomer to the Elisp wonderland. If the latter applies to you, I'd advise checking out David Wilson's [[https://www.youtube.com/c/SystemCrafters][System Crafters]] YouTube channel. * Some remarks I decided not to keep configs for features that I do not use anymore because this config is already huge. But here are the last commits that had these features presented. | Feature | Last commit | |--------------------------+------------------------------------------| | org-roam dailies | d2648918fcc338bd5c1cd6d5c0aa60a65077ccf7 | | org-roam projects | 025278a1e180e86f3aade20242e4ac1cdc1a2f13 | | treemacs | 3d87852745caacc0863c747f1fa9871d367240d2 | | tab-bar.el | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | spaceline | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | code compass | 8594d6f53e42c70bbf903e168607841854818a38 | | vue-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | svelte-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | pomidor | 8594d6f53e42c70bbf903e168607841854818a38 | | elfeed-score | 8e591e0d2afd909ae5be00caf17f9b17c6cd8b61 | | org-trello | 3f5967a5f63928ea9c8567d8d9f31e84cdbbc21f | | jabber | 9b0e73a4703ff35a2d30fd704200052888191217 | | wallabag | 9b0e73a4703ff35a2d30fd704200052888191217 | | conda | 609fc84e439b11ea5064f3a948079daebb654aca | | notmuch tags keybindings | eac134c5456051171c1c777254f503cc71ce12cd | | expand-region | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | org-latex-impatient | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | dired-single | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | progidy | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | tree-sitter | 1920a48aec49837d63fa88ca315928dc4e9d14c2 | | org-roam-protocol | 2f0c20eb01b8899d00d129cc7ca5c6b263c69c65 | | eshell-info-banner | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 | | aweshell | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 | * Initial setup Setting up the environment, performance tuning and a few basic settings. First things first, lexical binding. #+begin_src emacs-lisp ;;; -*- lexical-binding: t -*- #+end_src ** Packages *** straight.el Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo. The following is the bootstrap script of =straight.el=. References: - [[https://github.com/raxod502/straight.el][straight.el repo]] #+begin_src emacs-lisp :noweb-ref minimal (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)) #+end_src *** use-package A macro to simplify package specification & configuration. Integrates with straight.el. Set ~use-package-verbose~ to ~t~ to print out loading times for individual packages. References: - [[https://github.com/jwiegley/use-package][use-package repo]] #+begin_src emacs-lisp :noweb-ref minimal (straight-use-package 'use-package) (eval-when-compile (require 'use-package)) #+end_src ** Variables & environment This section is about optioning the Emacs config. The following is true if Emacs is meant to be used with TRAMP over slow ssh. Take a look at the [[*TRAMP][TRAMP]] section for more details. #+begin_src emacs-lisp (setq my/slow-ssh (or (string= (getenv "IS_TRAMP") "true"))) #+end_src The following is true is Emacs is run on a remote server where I don't need stuff like my org workflow #+begin_src emacs-lisp (setq my/remote-server (or (string= (getenv "IS_REMOTE") "true") (string= (system-name) "dev-digital") (string= (system-name) "viridian"))) #+end_src And the following is true if Emacs is run from termux on Android. #+begin_src emacs-lisp (setq my/is-termux (string-match-p (rx (* nonl) "com.termux" (* nonl)) (getenv "HOME"))) #+end_src 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: #+begin_src emacs-lisp (setenv "IS_EMACS" "true") #+end_src Finally, I want to have a minimal Emacs config for debugging purposes. This has just straight.el, use-packages, and evil. #+begin_src emacs-lisp :tangle ~/.emacs.d/init-minimal.el :noweb yes <> #+end_src To launch Emacs with this config, run #+begin_src bash :eval no :tangle no emacs -q -l ~/.emacs.d/init-minimal.el #+end_src A convinience macro: #+begin_src emacs-lisp (defmacro with-eval-after-load-norem (file &rest body) (declare (indent 1) (debug (form def-body))) `(unless my/remote-server (with-eval-after-load ,file ,@body))) #+end_src ** 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. #+begin_src emacs-lisp (setq my/emacs-started nil) (add-hook 'emacs-startup-hook (lambda () (message "*** Emacs loaded in %s with %d garbage collections." (format "%.2f seconds" (float-time (time-subtract after-init-time before-init-time))) gcs-done) (setq my/emacs-started t))) #+end_src 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. #+begin_src emacs-lisp ;; (setq use-package-verbose t) #+end_src *** Garbage collection Just setting ~gc-cons-treshold~ to a larger value. #+begin_src emacs-lisp (setq gc-cons-threshold 80000000) (setq read-process-output-max (* 1024 1024)) #+end_src *** 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. #+begin_src emacs-lisp (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)))) #+end_src *** Measure RAM usage I've noticed that Emacs occasionally eats a lot of RAM, especially when used with EXWM. This is my attempt to measure RAM usage. I have some concerns that =ps -o rss= may be unrepresentative because of [[https://stackoverflow.com/questions/131303/how-can-i-measure-the-actual-memory-usage-of-an-application-or-process][shared memory]], but I guess this shouldn't be a problem here because there's only one process of Emacs. #+begin_src emacs-lisp (defun my/get-ram-usage-async (callback) (let* ((temp-buffer (generate-new-buffer "*ps*")) (proc (start-process "ps" temp-buffer "ps" "-p" (number-to-string (emacs-pid)) "-o" "rss"))) (set-process-sentinel proc (lambda (process _msg) (when (eq (process-status process) 'exit) (let* ((output (with-current-buffer temp-buffer (buffer-string))) (usage (string-to-number (nth 1 (split-string output "\n"))))) (ignore-errors (funcall callback usage))) (kill-buffer temp-buffer)))))) (defun my/ram-usage () (interactive) (my/get-ram-usage-async (lambda (data) (message "%f Gb" (/ (float data) 1024 1024))))) #+end_src ** Micromamba [[https://github.com/mamba-org/mamba][mamba]] is a faster alternative to [[https://www.anaconda.com/][Anaconda]], a package and environment manager. =micromamba= is a tiny version that provides a subset of mamba commands. [[https://github.com/SqrtMinusOne/micromamba.el][micromamba.el]] is my package to interact with the latter. #+begin_src emacs-lisp (use-package micromamba :straight t :if (executable-find "micromamba") :config (micromamba-activate "general")) #+end_src ** 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= #+begin_src emacs-lisp (setq custom-file (concat user-emacs-directory "custom.el")) (load custom-file 'noerror) #+end_src *** authinfo Use only the gpg-encrypted version of the file. #+begin_src emacs-lisp (setq auth-source-debug nil) (setq auth-sources '("~/.authinfo.gpg")) #+end_src *** 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= #+begin_src emacs-lisp (let ((private-file (expand-file-name "private.el" user-emacs-directory))) (when (file-exists-p private-file) (load-file private-file))) #+end_src *** No littering By default Emacs and its packages create a lot files in =.emacs.d= and in other places. [[https://github.com/emacscollective/no-littering][no-littering]] is a collective effort to redirect all of that to two folders in =user-emacs-directory=. #+begin_src emacs-lisp (use-package no-littering :straight t) #+end_src ** Helper functions *** Run command in background I think I've copied it from somewhere. #+begin_src emacs-lisp (defun my/run-in-background (command) (let ((command-parts (split-string command "[ ]+"))) (apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts))))) #+end_src *** Close buffer and its windows #+begin_src emacs-lisp (defun my/quit-window-and-buffer () (interactive) (quit-window t)) #+end_src ** Prevent Emacs from closing This adds a confirmation to avoid accidental Emacs closing. #+begin_src emacs-lisp (setq confirm-kill-emacs 'y-or-n-p) #+end_src * General settings ** Keybindings *** general.el general.el provides a convenient interface to manage Emacs keybindings. References: - [[https://github.com/noctuid/general.el][general.el repo]] #+begin_src emacs-lisp (use-package general :straight t :config (general-evil-setup)) #+end_src *** 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: - [[https://github.com/justbur/emacs-which-key][which-key repo]] #+begin_src emacs-lisp (use-package which-key :config (setq which-key-idle-delay 0.3) (setq which-key-popup-type 'frame) (which-key-mode) (which-key-setup-side-window-bottom) (set-face-attribute 'which-key-local-map-description-face nil :weight 'bold) :straight t) #+end_src **** dump keybindings A function to dump keybindings starting with a prefix to a buffer in a tree-like form. #+begin_src emacs-lisp (defun my/dump-bindings-recursive (prefix &optional level buffer) (dolist (key (which-key--get-bindings (kbd prefix))) (with-current-buffer buffer (when level (insert (make-string level ? ))) (insert (apply #'format "%s%s%s\n" key))) (when (string-match-p (rx bos "+" (* nonl)) (substring-no-properties (elt key 2))) (my/dump-bindings-recursive (concat prefix " " (substring-no-properties (car key))) (+ 2 (or level 0)) buffer)))) (defun my/dump-bindings (prefix) "Dump keybindings starting with PREFIX in a tree-like form." (interactive "sPrefix: ") (let ((buffer (get-buffer-create "bindings"))) (with-current-buffer buffer (erase-buffer)) (my/dump-bindings-recursive prefix 0 buffer) (with-current-buffer buffer (goto-char (point-min)) (setq-local buffer-read-only t)) (switch-to-buffer-other-window buffer))) #+end_src *** 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: - [[https://github.com/emacs-evil/evil][evil repo]] - [[https://www.youtube.com/watch?v=JWD1Fpdd4Pc][(YouTube) Evil Mode: Or, How I Learned to Stop Worrying and Love Emacs]] **** Evil-mode Basic evil configuration. #+begin_src emacs-lisp :noweb-ref minimal (use-package evil :straight t :init (setq evil-want-integration t) (setq evil-want-C-u-scroll t) (setq evil-want-keybinding nil) (setq evil-search-module 'evil-search) (setq evil-split-window-below t) (setq evil-vsplit-window-right t) (unless (display-graphic-p) (setq evil-want-C-i-jump nil)) :config (evil-mode 1) ;; (setq evil-respect-visual-line-mode t) (when (fboundp #'undo-tree-undo) (evil-set-undo-system 'undo-tree)) (when (fboundp #'general-define-key) (general-define-key :states '(motion) "ze" nil))) #+end_src **** Addons [[https://github.com/emacs-evil/evil-surround][evil-surround]] emulates one of my favorite vim plugins, surround.vim. Adds a lot of parentheses management options. #+begin_src emacs-lisp (use-package evil-surround :straight t :after evil :config (global-evil-surround-mode 1)) #+end_src [[https://github.com/linktohack/evil-commentary][evil-commentary]] emulates commentary.vim. It provides actions for quick insertion and deletion of comments. #+begin_src emacs-lisp (use-package evil-commentary :straight t :after evil :config (evil-commentary-mode)) #+end_src [[https://github.com/blorbx/evil-quickscope][evil-quickscope]] emulates quickscope.vim. It highlights certain target characters for f, F, t, T keys. #+begin_src emacs-lisp (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))) #+end_src [[https://github.com/cofi/evil-numbers][evil-numbers]] allows incrementing and decrementing numbers at point. #+begin_src emacs-lisp (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)) #+end_src [[https://github.com/edkolev/evil-lion][evil-lion]] provides alignment operators, somewhat similar to vim-easyalign. #+begin_src emacs-lisp (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)) #+end_src [[https://github.com/redguardtoo/evil-matchit][evil-matchit]] makes "%" to match things like tags. It doesn't work perfectly, so I +occasionally+ turn it off. #+begin_src emacs-lisp (use-package evil-matchit :straight t :disabled :config (global-evil-matchit-mode 1)) #+end_src **** My additions Do ex search in other buffer. Like =*=, but switch to other buffer and search there. #+begin_src emacs-lisp (defun my/evil-ex-search-word-forward-other-window (count &optional symbol) (interactive (list (prefix-numeric-value current-prefix-arg) evil-symbol-word-search)) (save-excursion (evil-ex-start-word-search nil 'forward count symbol)) (other-window 1) (evil-ex-search-next)) (general-define-key :states '(normal) "&" #'my/evil-ex-search-word-forward-other-window) #+end_src **** evil-collection [[https://github.com/emacs-evil/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 [[https://github.com/emacs-evil/evil-collection/tree/master/modes][modes]] folder. #+begin_src emacs-lisp :noweb-ref minimal (use-package evil-collection :straight t :after evil :config (evil-collection-init '(eww devdocs proced emms pass calendar dired ivy debug guix calc docker ibuffer geiser pdf info elfeed edebug bookmark company vterm flycheck profiler cider explain-pause-mode notmuch custom xref eshell helpful compile comint git-timemachine magit prodigy slime forge deadgrep vc-annonate telega doc-view gnus outline))) #+end_src *** My keybindings Various keybinding settings that I can't put anywhere else. **** Escape key +Use the escape key instead of =C-g= whenever possible+ No, not really after 2 years... But I'll keep this fragment. 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. #+begin_src emacs-lisp (defun minibuffer-keyboard-quit () "Abort recursive edit. In Delete Selection mode, if the mark is active, just deactivate it; then it takes a second \\[keyboard-quit] to abort the minibuffer." (interactive) (if (and delete-selection-mode transient-mark-mode mark-active) (setq deactivate-mark t) (when (get-buffer "*Completions*") (delete-windows-on "*Completions*")) (abort-recursive-edit))) (defun my/escape-key () (interactive) (evil-ex-nohighlight) (keyboard-quit)) (general-define-key :keymaps '(normal visual global) [escape] #'my/escape-key) (general-define-key :keymaps '(minibuffer-local-map minibuffer-local-ns-map minibuffer-local-completion-map minibuffer-local-must-match-map minibuffer-local-isearch-map) [escape] 'minibuffer-keyboard-quit) #+end_src **** Home & end #+begin_src emacs-lisp (general-def :states '(normal insert visual) "" 'beginning-of-line "" 'end-of-line) #+end_src **** My leader Using the =SPC= key as a leader key, like in Doom Emacs or Spacemacs. #+begin_src emacs-lisp (general-create-definer my-leader-def :keymaps 'override :prefix "SPC" :states '(normal motion emacs)) (general-def :states '(normal motion emacs) "SPC" nil "M-SPC" (general-key "SPC")) (general-def :states '(insert) "M-SPC" (general-key "SPC" :state 'normal)) (my-leader-def "?" 'which-key-show-top-level) (my-leader-def "E" 'eval-expression) (general-def :states '(insert) " e" #'eval-expression) #+end_src =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=). #+begin_src emacs-lisp (my-leader-def "a" '(:which-key "apps")) #+end_src **** Universal argument Change the universal argument to =M-u=. I use =C-u= to scroll up, as I'm used to from vim. #+begin_src emacs-lisp (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) #+end_src **** Profiler The built-in profiler is a magnificent tool to troubleshoot performance issues. #+begin_src emacs-lisp (my-leader-def :infix "P" "" '(:which-key "profiler") "s" 'profiler-start "e" 'profiler-stop "p" 'profiler-report) #+end_src **** 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][i3 integration]]. #+begin_src emacs-lisp (general-define-key :keymaps 'override "C-" 'evil-window-right "C-" 'evil-window-left "C-" 'evil-window-up "C-" '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) #+end_src =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. #+begin_src emacs-lisp (winner-mode 1) (general-define-key :keymaps 'evil-window-map "u" 'winner-undo "U" 'winner-redo) #+end_src **** Buffer management #+begin_src emacs-lisp (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) #+end_src **** xref Some keybindings for xref and go to definition. #+begin_src emacs-lisp (general-nmap "gD" 'xref-find-definitions-other-window "gr" 'xref-find-references "gd" 'evil-goto-definition) (my-leader-def "fx" 'xref-find-apropos) #+end_src #+begin_src emacs-lisp (use-package xref :straight (:type built-in)) #+end_src **** 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 [[https://github.com/elp-revive/origami.el][origami.el]], which I found to be somewhat less stable. Evil does a pretty good job of abstracting all these packages with a set of vim-like keybindings. I was using =SPC= in vim, but as now this isn't an option, I set =TAB= to toggle folding. #+begin_src emacs-lisp (require 'hideshow) (general-define-key :keymaps '(hs-minor-mode-map outline-minor-mode-map) :states '(normal motion) "ze" 'hs-hide-level "TAB" 'evil-toggle-fold) #+end_src **** Zoom UI #+begin_src emacs-lisp (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) #+end_src ** 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 [[https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/][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 [[https://i3wm.org][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 [[https://xmonad.org/][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, [[http://www.qtile.org/][Qtile]], [[https://awesomewm.org/][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: #+begin_src bash :tangle ~/bin/scripts/emacs-i3-integration if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then command="(my/emacs-i3-integration \"$@\")" emacsclient -e "$command" else i3-msg $@ fi #+end_src This script is being run from the [[file:Desktop.org::*i3wm][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: #+BEGIN_SRC emacs-lisp (unless my/remote-server (add-hook 'after-init-hook #'server-start)) #+END_SRC And here is a simple macro to do the Emacs -> i3 part: #+begin_src emacs-lisp (defmacro i3-msg (&rest args) `(start-process "emacs-i3-windmove" nil "i3-msg" ,@args)) #+end_src 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 [[https://github.com/emacs-evil/evil][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. #+begin_src emacs-lisp (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)))) #+end_src For the =move= I want the following behavior: - if there is space in the required direction, move the Emacs window there; - if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions; - otherwise, move an X window (Emacs frame). For the first part, =window-swap-states= with =windmove-find-other-window= do well enough. =evil-move-window= works well for the second part. By itself it doesn't behave quite like i3, for instance, =(evil-move-window 'right)= in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here. So here is a simple predicate which checks whether there is space in the given direction. #+begin_src emacs-lisp (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))))) #+end_src And the implementation of the move command. #+begin_src emacs-lisp (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)))))) #+end_src 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. #+begin_src emacs-lisp (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))))))) #+end_src [[https://github.com/emacsorphanage/transpose-frame][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. #+begin_src emacs-lisp (use-package transpose-frame :straight t :commands (transpose-frame)) #+end_src 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. #+begin_src emacs-lisp (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)))) #+end_src ** Editing text Various packages, tricks, and settings that help with the central task of Emacs - editing text. *** Indentation & whitespace **** Aggressive Indent A package to keep the code intended. Doesn't work too well with many ecosystems because the LSP-based indentation is rather slow but nice for Lisps. References: - [[https://github.com/Malabarba/aggressive-indent-mode][aggressive-indent-mode repo]] #+begin_src emacs-lisp (use-package aggressive-indent :commands (aggressive-indent-mode) :straight t) #+end_src **** Delete trailing whitespace Delete trailing whitespace on save, unless in particular modes where trailing whitespace is important, like Markdown. #+begin_src emacs-lisp (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)))) #+end_src **** Tabs Some default settings to manage tabs. #+begin_src emacs-lisp (setq tab-always-indent nil) (setq-default default-tab-width 4) (setq-default tab-width 4) (setq-default evil-indent-convert-tabs nil) (setq-default indent-tabs-mode nil) (setq-default evil-shift-round nil) #+end_src *** Settings **** Scrolling #+begin_src emacs-lisp (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) #+end_src **** Clipboard #+begin_src emacs-lisp (setq select-enable-clipboard t) (setq mouse-yank-at-point t) #+end_src **** Backups #+begin_src emacs-lisp (setq backup-inhibited t) (setq auto-save-default nil) #+end_src *** Undo Tree Replaces Emacs built-in sequential undo system with a tree-based one. Probably one of the greatest options of Emacs as a text editor. References: - [[https://www.emacswiki.org/emacs/UndoTree][UndoTree on EmacsWiki]] #+begin_src emacs-lisp (use-package undo-tree :straight t :config (global-undo-tree-mode) (evil-set-undo-system 'undo-tree) (setq undo-tree-visualizer-diff t) (setq undo-tree-visualizer-timestamps t) (setq undo-tree-auto-save-history nil) (my-leader-def "u" 'undo-tree-visualize) (fset 'undo-auto-amalgamate 'ignore) (setq undo-limit 6710886400) (setq undo-strong-limit 100663296) (setq undo-outer-limit 1006632960)) #+end_src *** Snippets A snippet system for Emacs and a collection of pre-built snippets. ~yasnippet-snippets~ has to be loaded before ~yasnippet~ for user snippets to override the pre-built ones. Edit <2022-04-11 Mon> I don't really use ~yasnippet-snippets~, so I'd rather write stuff manually. References: - [[http://joaotavora.github.io/yasnippet/][yasnippet documentation]] #+begin_src emacs-lisp (use-package yasnippet-snippets :disabled :straight t) (use-package yasnippet :straight t :config (setq yas-snippet-dirs `(,(concat (expand-file-name user-emacs-directory) "snippets") ;; yasnippet-snippets-dir )) (setq yas-triggers-in-field t) (yas-global-mode 1) (my-leader-def :keymaps 'yas-minor-mode-map :infix "es" "" '(:wk "yasnippet") "n" #'yas-new-snippet "s" #'yas-insert-snippet "v" #'yas-visit-snippet-file)) (general-imap "M-TAB" 'company-yasnippet) #+end_src *** Input Method #+begin_quote I have to switch layouts all the time, especially in LaTeX documents, because for some reason the Bolsheviks abandoned the idea of replacing Russian Cyrillic letters with Latin ones. #+end_quote - Me, [2021-04-24 Sat], in a commit to [[https://github.com/SystemCrafters/crafter-configs][SystemCrafters/crafter-configs]]. Fortunately, Emacs offers a way out of the above with input methods. References: - https://protesilaos.com/codelog/2023-12-12-emacs-multilingual-editing/ - A video by Prot from which I learned about this feature. #+begin_src emacs-lisp (setq default-input-method "russian-computer") #+end_src I also want to call =xkb-switch= in EXWM buffers with the same keybindig. | Guix dependency | |-----------------| | xkb-switch | #+begin_src emacs-lisp (defun my/toggle-input-method () (interactive) (if (derived-mode-p 'exwm-mode) (my/run-in-background "xkb-switch -n") (if (equal (string-trim (shell-command-to-string "xkb-switch -p")) "us") (toggle-input-method) (my/run-in-background "xkb-switch -s us")))) #+end_src =M-x delete-horizontal-space= doesn't feel that useful to me. #+begin_src emacs-lisp (general-define-key :keymaps 'global "M-\\" #'my/toggle-input-method) #+end_src *** 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: - [[https://github.com/Fuco1/smartparens][smartparens repo]] #+begin_src emacs-lisp (use-package smartparens :straight t) #+end_src **** Visual fill column mode #+begin_src emacs-lisp (use-package visual-fill-column :straight t :commands (visual-fill-column-mode) :config ;; How did it get here? ;; (add-hook 'visual-fill-column-mode-hook ;; (lambda () (setq visual-fill-column-center-text t))) ) #+end_src **** Accents Input accented characters. #+begin_src emacs-lisp (use-package accent :straight (:host github :repo "eliascotto/accent") :init (general-define-key :states '(normal) "gs" #'accent-company) (general-define-key :states '(normal insert) "M-n" #'accent-company) :commands (accent-menu) :config (general-define-key :keymaps 'popup-menu-keymap "C-j" #'popup-next "C-k" #'popup-previous "M-j" #'popup-next "M-k" #'popup-previous) (setq accent-custom '((a (ā)) (A (Ā))))) #+end_src ** Working with projects Packages related to managing projects. I used to have [[https://github.com/Alexander-Miller/treemacs][Treemacs]] here, but in the end decided that dired with [[https://github.com/jojojames/dired-sidebar][dired-sidebar]] does a better job. Dired has its separate section in "Applications". *** Projectile [[https://github.com/bbatsov/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. #+begin_src emacs-lisp (use-package projectile :straight t :config (projectile-mode +1) (setq projectile-project-search-path '("~/Code" "~/Documents"))) (use-package counsel-projectile :after (counsel projectile) :straight t) (my-leader-def "p" '(:keymap projectile-command-map :which-key "projectile")) (general-nmap "C-p" 'counsel-projectile-find-file) #+end_src *** Git & Magit [[https://magit.vc/][Magit]] is a git interface for Emacs. The closest non-Emacs alternative (sans actual clones) I know is [[https://github.com/jesseduffield/lazygit][lazygit]], which I used before Emacs. #+begin_src emacs-lisp (use-package magit :straight t :commands (magit-status magit-file-dispatch) :init (my-leader-def "m" 'magit "M" 'magit-file-dispatch) :config (require 'forge) (setq magit-blame-styles '((headings (heading-format . "%-20a %C %s\n")) (highlight (highlight-face . magit-blame-highlight)) (lines (show-lines . t) (show-message . t))))) #+end_src [[https://github.com/emacsorphanage/git-gutter][git-gutter]] is a package which shows git changes for each line (added/changed/deleted lines). #+begin_src emacs-lisp (use-package git-gutter :straight t :if (not my/slow-ssh) :config (global-git-gutter-mode +1)) #+end_src [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] allows visiting previous versions of a file. #+begin_src emacs-lisp (use-package git-timemachine :straight t :commands (git-timemachine)) #+end_src *** Forge and code-review [[https://github.com/magit/forge][forge]] provides integration with forges, such as GitHub and GitLab. #+begin_src emacs-lisp (use-package forge :after magit :straight t :config (add-to-list 'forge-alist '("gitlab.etu.ru" "gitlab.etu.ru/api/v4" "gitlab.etu.ru" forge-gitlab-repository))) #+end_src =forge= depends on a package called [[https://github.com/magit/ghub][ghub]]. I don't like that it uses =auth-source= to store the token so I'll advise it to use =password-store=. #+begin_src emacs-lisp (defun my/password-store-get-field (entry field) (if-let (field (password-store-get-field entry field)) field (my/password-store-get-field entry field))) (defun my/ghub--token (host username package &optional nocreate forge) (cond ((and (or (equal host "gitlab.etu.ru/api/v4") (equal host "gitlab.etu.ru/api")) (equal username "pvkorytov")) (my/password-store-get-field "Job/Digital/Infrastructure/gitlab.etu.ru" (format "%s-token" package))) (t (error "Don't know token: %s %s %s" host username package)))) (with-eval-after-load 'ghub (advice-add #'ghub--token :override #'my/ghub--token)) #+end_src [[https://github.com/wandersoncferreira/code-review][code-review]] is a package that implements code review in Emacs. The main branch is broken, [[https://github.com/wandersoncferreira/code-review/pull/246][but this PR]] works. #+begin_src emacs-lisp (use-package code-review :straight (:host github :repo "phelrine/code-review" :branch "fix/closql-update") :after forge :config (setq code-review-auth-login-marker 'forge) (setq code-review-gitlab-base-url "gitlab.etu.ru") (setq code-review-gitlab-host "gitlab.etu.ru/api") (setq code-review-gitlab-graphql-host "gitlab.etu.ru/api") (general-define-key :states '(normal visual) :keymaps '(code-review-mode-map) "RET" #'code-review-comment-add-or-edit "gr" #'code-review-reload "r" #'code-review-transient-api "s" #'code-review-comment-code-suggestion "d" #'code-review-submit-single-diff-comment-at-point "TAB" #'magit-section-toggle) (general-define-key :states '(normal) :keymaps '(forge-topic-mode-map) "M-RET" #'code-review-forge-pr-at-point)) #+end_src Fix issue [[https://github.com/wandersoncferreira/code-review/issues/253][253]]: #+begin_src emacs-lisp (defun my/code-review-comment-quit () "Quit the comment window." (interactive) (magit-mode-quit-window t) (with-current-buffer (get-buffer code-review-buffer-name) (goto-char code-review-comment-cursor-pos) (code-review-comment-reset-global-vars))) (with-eval-after-load 'code-review (advice-add #'code-review-comment-quit :override #'my/code-review-comment-quit)) #+end_src *** Editorconfig Editorconfig support for Emacs. References: - [[https://editorconfig.org/][Editorconfig reference]] #+begin_src emacs-lisp (use-package editorconfig :straight t :config (unless my/slow-ssh (editorconfig-mode 1)) (add-to-list 'editorconfig-indentation-alist '(emmet-mode emmet-indentation))) #+end_src *** Editing files A minor mode to remember recently edited files. #+begin_src emacs-lisp (recentf-mode 1) #+end_src Save the last place visited in the file. #+begin_src emacs-lisp (save-place-mode nil) #+end_src *** Deadgrep [[https://github.com/Wilfred/deadgrep][deadgrep]] is a nice Emacs interface for [[https://github.com/BurntSushi/ripgrep][ripgrep]]. Running =ivy-occur= in =counsel-rg= does something a bit similar, but the deadgrep is more full-featured. Somehow I couldn't hook =toogle-truncate-lines= into the existing package hooks, so here goes advice. #+begin_src emacs-lisp (defun my/deadgrep-fix-buffer-advice (fun &rest args) (let ((buf (apply fun args))) (with-current-buffer buf (toggle-truncate-lines 1)) buf)) (use-package deadgrep :straight t :commands (deadgrep) :config (advice-add #'deadgrep--buffer :around #'my/deadgrep-fix-buffer-advice)) #+end_src ** Navigation Things to navigate in Emacs. *** Registers References: - [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Registers.html][Registers (GNU Emacs Manual)]] Somehow there's no built-in function to clear a register. #+begin_src emacs-lisp (defun my/register-clear (register) (interactive (list (register-read-with-preview "Clear register: "))) (setq register-alist (delq (assoc register register-alist) register-alist))) #+end_src #+begin_src emacs-lisp (setq register-preview-delay which-key-idle-delay) (my-leader-def :infix "g" "" '(:wk "registers & marks") "y" #'copy-to-register "p" #'insert-register "o" #'point-to-register "c" #'my/register-clear "r" #'jump-to-register "R" #'counsel-register "w" #'window-configuration-to-register) #+End_src *** Marks References: - [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Mark.html][The Mark and the Region (GNU Emacs Manual)]] - [[https://www.masteringemacs.org/article/fixing-mark-commands-transient-mark-mode][Fixing the mark commands in transient mark mode - Mastering Emacs]] =transient-mark-mode= makes using marks for navigation a bit more cumbersome, but I'm not sure of potential side effects of disabling it... As of now, I want only to push a mark without activating it, so here's a function for that (taken from Mickey Peterson's article): #+begin_src emacs-lisp (defun my/push-mark-no-activate () "Pushes `point' to `mark-ring' and does not activate the region Equivalent to \\[set-mark-command] when \\[transient-mark-mode] is disabled" (interactive) (push-mark (point) t nil) (message "Pushed mark to ring")) #+end_src Also a function to clear the current mark ring. #+begin_src emacs-lisp (defun my/mark-ring-clear () (interactive) (setq mark-ring nil)) #+end_src A variant of =counsel-mark-ring= to work with =global-mark-ring=: #+begin_src emacs-lisp (defun my/counsel-global-mark-ring () "Browse `mark-ring' interactively. Obeys `widen-automatically', which see." (interactive) (let* ((counsel--mark-ring-calling-point (point)) (marks (copy-sequence global-mark-ring)) (marks (delete-dups marks)) (candidates (counsel-mark--get-candidates marks))) (if candidates (counsel-mark--ivy-read "Mark: " candidates 'my/counsel-global-mark-ring) (message "Mark ring is empty")))) #+end_src Keybindings: #+begin_src emacs-lisp (my-leader-def :infix "g" "g" #'counsel-mark-ring "G" #'my/counsel-global-mark-ring "C" #'my/mark-ring-clear) (general-define-key :keymaps 'global "C-SPC" #'my/push-mark-no-activate) #+end_src *** Avy [[https://github.com/abo-abo/avy][Avy]] is a package that helps navigate Emacs in a tree-like manner. References: - [[https://karthinks.com/software/avy-can-do-anything/][Avy can do anything]] #+begin_src emacs-lisp (use-package avy :straight t :config (setq avy-timeout-seconds 0.5) (setq avy-ignored-modes '(image-mode doc-view-mode pdf-view-mode exwm-mode)) (general-define-key :states '(normal motion) "-" nil "--" #'avy-goto-char-2 "-=" #'avy-goto-symbol-1)) #+End_src [[https://github.com/abo-abo/ace-link][ace-link]] is a package to jump to links with avy. #+begin_src emacs-lisp (use-package ace-link :straight t :commands (ace-link-info ace-link-help ace-link-woman ace-link-eww)) #+end_src ** Completion *** Ivy, counsel, swiper Minibuffer completion tools for Emacs. References: - [[https://oremacs.com/swiper/][repo]] - [[https://oremacs.com/swiper/][User Manual]] #+begin_src emacs-lisp (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) #+end_src *** ivy-rich [[https://github.com/Yevgnen/ivy-rich][ivy-rich]] provides a more informative interface for ivy. #+begin_src emacs-lisp (use-package ivy-rich :straight t :after ivy :config (ivy-rich-mode 1) (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line)) #+end_src *** prescient A package that enhances sorting & filtering of candidates. =ivy-prescient= adds integration with Ivy. References: - [[https://github.com/raxod502/prescient.el][prescient.el repo]] #+begin_src emacs-lisp :noweb yes (use-package ivy-prescient :straight t :after counsel :config (ivy-prescient-mode +1) (setq ivy-prescient-retain-classic-highlighting t) (prescient-persist-mode 1) (setq ivy-prescient-sort-commands '(:not swiper swiper-isearch ivy-switch-buffer ;; ivy-resume ;; ivy--restore-session lsp-ivy-workspace-symbol dap-switch-stack-frame my/dap-switch-stack-frame dap-switch-session dap-switch-thread counsel-grep ;; counsel-find-file counsel-git-grep counsel-rg counsel-ag counsel-ack counsel-fzf counsel-pt counsel-imenu counsel-yank-pop counsel-recentf counsel-buffer-or-recentf proced-filter-interactive proced-sort-interactive perspective-exwm-switch-perspective my/persp-ivy-switch-buffer-other-window lsp-execute-code-action dired-recent-open org-ql-view my/index-nav org-set-effort)) ;; Do not use prescient in find-file (ivy--alist-set 'ivy-sort-functions-alist #'read-file-name-internal #'ivy-sort-file-function-default)) #+end_src *** keybindings Setting up quick access to various completions. #+begin_src emacs-lisp (my-leader-def :infix "f" "" '(:which-key "various completions")' ;; "b" 'counsel-switch-buffer "b" 'persp-ivy-switch-buffer "e" 'micromamba-activate "f" 'project-find-file "c" 'counsel-yank-pop "a" 'counsel-rg "d" 'deadgrep "A" 'counsel-ag) (general-define-key :states '(insert normal) "C-y" 'counsel-yank-pop) (defun my/swiper-isearch () (interactive) (if current-prefix-arg (swiper-all) (swiper-isearch))) (my-leader-def "SPC SPC" 'ivy-resume) (my-leader-def "s" 'my/swiper-isearch) (general-define-key :keymaps '(ivy-minibuffer-map swiper-map) "M-j" 'ivy-next-line "M-k" 'ivy-previous-line "" 'ivy-call "M-RET" 'ivy-immediate-done [escape] 'minibuffer-keyboard-quit) #+end_src *** company A completion framework for Emacs. References: - [[http://company-mode.github.io/][company homepage]] - [[https://github.com/sebastiencs/company-box][company-box homepage]] #+begin_src emacs-lisp (use-package company :straight t :config (global-company-mode) (setq company-idle-delay 0.2) (setq company-dabbrev-downcase nil) (setq company-show-numbers t)) (general-imap "C-SPC" 'company-complete) #+end_src A company frontend with nice icons. +Disabled since the base company got icons support and since company-box has some issues with spaceline.+ Enabled back because I didn't like spaceline. #+begin_src emacs-lisp (use-package company-box :straight t :if (display-graphic-p) :after (company) :hook (company-mode . company-box-mode)) #+end_src ** Help - *CREDIT*: Thanks @phundrak on the System Crafters Discord for suggesting =help-map= [[https://github.com/Wilfred/helpful][helpful]] package improves the =*help*= buffer. #+begin_src emacs-lisp (use-package helpful :straight t :commands (helpful-callable helpful-variable helpful-key helpful-macro helpful-function helpful-command)) #+end_src As I use =C-h= to switch buffers, I moved the help to =SPC-h= with the code below. #+begin_src emacs-lisp (my-leader-def "h" '(:keymap help-map :which-key "help")) (my-leader-def :infix "h" "" '(:which-key "help") "h" '(:keymap help-map :which-key "help-map") "f" 'helpful-function "k" 'helpful-key "v" 'helpful-variable "o" 'helpful-symbol "i" 'info) (general-define-key :keymaps 'help-map "f" 'helpful-function "k" 'helpful-key "v" 'helpful-variable "o" 'helpful-symbol) #+end_src ** Time trackers Time trackers I happen to use. References: - [[https://wakatime.com][WakaTime]] - [[https://activitywatch.net/][ActivityWatch]] *** WakaTime Before I figure out how to package this for Guix: - Clone [[https://github.com/wakatime/wakatime-cli][the repo]] - Run ~go build~ - Copy the binary to the =~/bin= folder #+begin_src emacs-lisp :noweb yes (use-package wakatime-mode :straight (:host github :repo "SqrtMinusOne/wakatime-mode") :if (not (or my/remote-server)) :config (setq wakatime-ignore-exit-codes '(0 1 102 112)) (advice-add 'wakatime-init :after (lambda () (setq wakatime-cli-path (expand-file-name "~/bin/wakatime-cli")))) (when (file-exists-p "~/.wakatime.cfg") (setq wakatime-api-key (string-trim (shell-command-to-string "awk '/api-key/{print $NF}' ~/.wakatime.cfg")))) ;; (setq wakatime-cli-path (executable-find "wakatime")) (global-wakatime-mode)) #+end_src *** ActivityWatch #+begin_src emacs-lisp (use-package request :straight t :defer t) (use-package activity-watch-mode :straight t :if (not (or my/is-termux my/remote-server)) :config (global-activity-watch-mode)) #+end_src * UI settings ** General settings *** Miscellaneous Disable GUI elements #+begin_src emacs-lisp (unless my/is-termux (tool-bar-mode -1) (menu-bar-mode -1) (scroll-bar-mode -1)) (when my/is-termux (menu-bar-mode -1)) #+end_src Transparency. Not setting it here, as I used to use [[file:Desktop.org::*Picom][picom]] with i3, and EXWM config has its own settings. #+begin_src emacs-lisp ;; (set-frame-parameter (selected-frame) 'alpha '(90 . 90)) ;; (add-to-list 'default-frame-alist '(alpha . (90 . 90))) #+end_src Prettify symbols. Also not setting it, ligatures seem to be enough for me. #+begin_src emacs-lisp ;; (global-prettify-symbols-mode) #+end_src Do not show GUI dialogs #+begin_src emacs-lisp (setq use-dialog-box nil) #+end_src No start screen #+begin_src emacs-lisp (setq inhibit-startup-screen t) #+end_src Visual bell #+begin_src emacs-lisp (setq visible-bell 0) #+end_src y or n instead of yes or no #+begin_src emacs-lisp (defalias 'yes-or-no-p 'y-or-n-p) #+end_src Hide mouse cursor while typing #+begin_src emacs-lisp (setq make-pointer-invisible t) #+end_src Show pairs #+begin_src emacs-lisp (show-paren-mode 1) #+end_src Highlight the current line #+begin_src emacs-lisp (global-hl-line-mode 1) #+end_src *** Line numbers Line numbers. There seems to be a catch with the relative number setting: - =visual= doesn't take folding into account but also doesn't take wrapped lines into account (i.e. there are multiple numbers for a single wrapped line) - =relative= makes a single number for a wrapped line, but counts folded lines. =visual= option seems to be less of a problem in most cases. #+begin_src emacs-lisp (global-display-line-numbers-mode 1) (line-number-mode nil) (setq display-line-numbers-type 'visual) (column-number-mode) #+end_src *** Word wrapping Word wrapping. These settings aren't too obvious compared to =:set wrap= from vim: - =word-wrap= means just "don't split one word between two lines". So, if there isn't enough place to put a word at the end of the line, it will be put on a new one. Run =M-x toggle-word-wrap= to toggle that. - =visual-line-mode= seems to be a superset of =word-wrap=. It also enables some editing commands to work on visual lines instead of logical ones, hence the naming. - =auto-fill-mode= does the same as =word-wrap=, except it actually *edits the buffer* to make lines break in the appropriate places. - =truncate-lines= truncates long lines instead of continuing them. Run =M-x toggle-truncate-lines= to toggle that. I find that =truncate-lines= behaves strangely when =visual-line-mode= is on, so I use one or another. #+begin_src emacs-lisp (setq word-wrap 1) (global-visual-line-mode 1) #+end_src *** Custom frame format Title format, which used to look something like =emacs:project@hostname=. Now it's just =emacs=. #+begin_src emacs-lisp (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))))) )) #+end_src *** Olivetti [[https://github.com/rnkn/olivetti][Olivetti]] is a package that limits the current text body width. It's pretty nice to use when writing texts. #+begin_src emacs-lisp (use-package olivetti :straight t :if (display-graphic-p) :config (setq-default olivetti-body-width 86)) #+end_src *** Keycast Showing the last pressed key. Occasionally useful. #+begin_src emacs-lisp (use-package keycast :straight t :config (define-minor-mode keycast-mode "Keycast mode" :global t (if keycast-mode (progn (add-to-list 'global-mode-string '("" keycast-mode-line " ")) (add-hook 'pre-command-hook 'keycast--update t) ) (remove-hook 'pre-command-hook 'keycast--update) (setq global-mode-string (delete '("" keycast-mode-line " ") global-mode-string))))) #+end_src ** Themes and colors *** Theme packages My colorschemes of choice. #+begin_src emacs-lisp (use-package doom-themes :straight t :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)) #+end_src #+begin_src emacs-lisp (use-package modus-themes :straight t) #+end_src Let's see... #+begin_src emacs-lisp (use-package ef-themes :straight t :config (setq ef-duo-light-palette-overrides '((constant green)))) #+end_src *** Custom theme Here I define a few things on the top of Emacs theme, because: - Occasionally I want to have more theme-derived faces - I also want Emacs theme to be applied to the rest of the system (see the [[file:Desktop.org][Desktop]] config on that) Theme-derived faces have to placed in a custom theme, because if one calls =custom-set-faces= and =custom-set-variables= in code, whenever a variable is changed and saved in a customize buffer, data from all calls of these functions is saved as well. **** Get color values Here's a great package with various color tools: #+begin_src emacs-lisp (use-package ct :straight t) #+end_src As of now I want this to support =doom-themes= and =modus-themes=. So, let's get which one is enabled: #+begin_src emacs-lisp (defun my/doom-p () (seq-find (lambda (x) (string-match-p (rx bos "doom") (symbol-name x))) custom-enabled-themes)) (defun my/modus-p () (seq-find (lambda (x) (string-match-p (rx bos "modus") (symbol-name x))) custom-enabled-themes)) (defun my/ef-p () (seq-find (lambda (x) (string-match-p (rx bos "ef") (symbol-name x))) custom-enabled-themes)) #+end_src I also want to know if the current theme is light or not: #+begin_src emacs-lisp (defun my/light-p () (ct-light-p (my/color-value 'bg))) (defun my/dark-p () (not (my/light-p))) #+end_src Now, let's get the current color from =doom=. =doom-themes= provide =doom-color=, but I also want to: - override some colors - add =black=, =white=, =light-*= and =border= #+begin_src emacs-lisp (defconst my/theme-override '((doom-palenight (red . "#f07178")))) (defvar my/alpha-for-light 7) (defun my/doom-color (color) (when (doom-color 'bg) (let ((override (alist-get (my/doom-p) my/theme-override)) (color-name (symbol-name color)) (is-light (ct-light-p (doom-color 'bg)))) (or (alist-get color override) (cond ((eq 'black color) (if is-light (doom-color 'fg) (doom-color 'bg))) ((eq 'white color) (if is-light (doom-color 'bg) (doom-color 'fg))) ((eq 'border color) (if is-light (doom-color 'base0) (doom-color 'base8))) ((string-match-p (rx bos "light-") color-name) (ct-edit-hsl-l-inc (my/doom-color (intern (substring color-name 6))) my/alpha-for-light)) (t (doom-color color))))))) #+end_src And the same for =modus-themes=. =my/modus-color= has to accept the same arguments as I use for =my/doom-color= for backward compatibility, which requires a bit more tuning. #+begin_src emacs-lisp (defun my/modus-get-base (color) (let ((base-value (string-to-number (substring (symbol-name color) 4 5))) (base-start (cadr (assoc 'bg-main (modus-themes--current-theme-palette)))) (base-end (cadr (assoc 'fg-dim (modus-themes--current-theme-palette))))) (nth base-value (ct-gradient 9 base-start base-end t)))) (defun my/prot-color (color palette) (let ((is-light (ct-light-p (cadr (assoc 'bg-main palette))))) (cond ((member color '(black white light-black light-white)) (let ((bg-main (cadr (assoc 'bg-main palette))) (fg-main (cadr (assoc 'fg-main palette)))) (pcase color ('black (if is-light fg-main bg-main)) ('white (if is-light bg-main fg-main)) ('light-black (ct-edit-hsl-l-inc (if is-light fg-main bg-main) 15)) ('light-white (ct-edit-hsl-l-inc (if is-light bg-main fg-main) 15))))) ((or (eq color 'bg)) (cadr (assoc 'bg-main palette))) ((or (eq color 'fg)) (cadr (assoc 'fg-main palette))) ((eq color 'bg-alt) (cadr (assoc 'bg-dim palette))) ((eq color 'violet) (cadr (assoc 'magenta-cooler palette))) ((string-match-p (rx bos "base" digit) (symbol-name color)) (my/modus-get-base color)) ((string-match-p (rx bos "dark-") (symbol-name color)) (cadr (assoc (intern (format "%s-cooler" (substring (symbol-name color) 5))) palette))) ((eq color 'grey) (my/modus-get-base 'base5)) ((string-match-p (rx bos "light-") (symbol-name color)) (or (cadr (assoc (intern (format "%s-intense" (substring (symbol-name color) 6))) palette)) (cadr (assoc (intern (format "bg-%s-intense" (substring (symbol-name color) 6))) palette)))) (t (cadr (assoc color palette)))))) (defun my/modus-color (color) (my/prot-color color (modus-themes--current-theme-palette))) (defun my/ef-color (color) (my/prot-color color (ef-themes--current-theme-palette))) #+end_src Test the three functions. #+begin_src emacs-lisp (defconst my/test-colors-list '(black red green yellow blue magenta cyan white light-black light-red light-green light-yellow light-blue light-magenta light-cyan light-white bg fg violet grey base0 base1 base2 base3 base4 base5 base6 base7 base8 border bg-alt)) (defun my/test-colors () (interactive) (let ((buf (generate-new-buffer "*colors-test*"))) (with-current-buffer buf (insert (format "%-20s %-10s %-10s %-10s" "Color" "Doom" "Modus" "Ef") "\n") (cl-loop for color in my/test-colors-list do (insert (format "%-20s %-10s %-10s %-10s\n" (prin1-to-string color) (my/doom-color color) (my/modus-color color) (my/ef-color color)))) (special-mode) (rainbow-mode)) (switch-to-buffer buf))) #+end_src Finally, one function to get the value of a color in the current theme. #+begin_src emacs-lisp (defun my/color-value (color) (cond ((stringp color) (my/color-value (intern color))) ((eq color 'bg-other) (or (my/color-value 'bg-dim) (let ((color (my/color-value 'bg))) (if (ct-light-p color) (ct-edit-hsl-l-dec color 2) (ct-edit-hsl-l-dec color 3))))) ((my/doom-p) (my/doom-color color)) ((my/modus-p) (my/modus-color color)) ((my/ef-p) (my/ef-color color)))) #+end_src And a few more functions **** Custom theme So, the custom theme: #+begin_src emacs-lisp (deftheme my-theme-1) #+end_src A macro to simplify defining custom colors. #+begin_src emacs-lisp (defvar my/my-theme-update-color-params nil) (defmacro my/use-colors (&rest data) `(progn ,@(cl-loop for i in data collect `(setf (alist-get ',(car i) my/my-theme-update-color-params) (list ,@(cl-loop for (key value) on (cdr i) by #'cddr append `(,key ',value))))) (when (and (or (my/doom-p) (my/modus-p)) my/emacs-started) (my/update-my-theme)))) #+end_src This macro puts lambdas to =my/my-theme-update-colors-hook= that updates faces in =my-theme-1=. Now I have to call this hook: #+begin_src emacs-lisp (defun my/update-my-theme (&rest _) (interactive) (cl-loop for (face . values) in my/my-theme-update-color-params do (custom-theme-set-faces 'my-theme-1 `(,face ((t ,@(cl-loop for (key value) on values by #'cddr collect key collect (eval value))))))) (enable-theme 'my-theme-1)) (unless my/is-termux (advice-add 'load-theme :after #'my/update-my-theme) (add-hook 'emacs-startup-hook #'my/update-my-theme)) #+end_src Defining colors for =tab-bar.el=: #+begin_src emacs-lisp (my/use-colors (tab-bar-tab :background (my/color-value 'bg) :foreground (my/color-value 'yellow) :underline (my/color-value 'yellow)) (tab-bar :background nil :foreground nil) (magit-section-secondary-heading :foreground (my/color-value 'blue) :weight 'bold)) #+end_src **** Switch theme The built-in =load-theme= does not deactivate the previous theme, so here's a function that does that: #+begin_src emacs-lisp (defun my/switch-theme (theme) (interactive (list (intern (completing-read "Load custom theme: " (mapcar #'symbol-name (custom-available-themes)))))) (cl-loop for enabled-theme in custom-enabled-themes if (not (or (eq enabled-theme 'my-theme-1) (eq enabled-theme theme))) do (disable-theme enabled-theme)) (load-theme theme t) (when current-prefix-arg (my/regenerate-desktop))) #+end_src #+begin_src emacs-lisp (my/switch-theme 'ef-duo-light) #+end_src **** Extending current theme Colors that aren't set in themes. #+begin_src emacs-lisp (with-eval-after-load 'transient (my/use-colors (transient-key-exit :foreground (my/color-value 'dark-red)) (transient-key-noop :foreground (my/color-value 'grey)) (transient-key-return :foreground (my/color-value 'yellow)) (transient-key-stay :foreground (my/color-value 'green)))) #+end_src *** Dim inactive buffers Dim inactive buffers. #+begin_src emacs-lisp (use-package auto-dim-other-buffers :straight t :if (display-graphic-p) :config (auto-dim-other-buffers-mode t) (my/use-colors (auto-dim-other-buffers-face :background (my/color-value 'bg-other)))) #+end_src *** ANSI colors =ansi-color.el= is a built-in Emacs package that translates ANSI color escape codes into faces. It is used by many other packages but doesn't seem to have an integration with =doom-themes=, so here is one. #+begin_src emacs-lisp (with-eval-after-load 'ansi-color (my/use-colors (ansi-color-black :foreground (my/color-value 'base2) :background (my/color-value 'base0)) (ansi-color-red :foreground (my/color-value 'red) :background (my/color-value 'red)) (ansi-color-green :foreground (my/color-value 'green) :background (my/color-value 'green)) (ansi-color-yellow :foreground (my/color-value 'yellow) :background (my/color-value 'yellow)) (ansi-color-blue :foreground (my/color-value 'dark-blue) :background (my/color-value 'dark-blue)) (ansi-color-magenta :foreground (my/color-value 'violet) :background (my/color-value 'violet)) (ansi-color-cyan :foreground (my/color-value 'dark-cyan) :background (my/color-value 'dark-cyan)) (ansi-color-white :foreground (my/color-value 'base8) :background (my/color-value 'base8)) (ansi-color-bright-black :foreground (my/color-value 'base5) :background (my/color-value 'base5)) (ansi-color-bright-red :foreground (my/color-value 'orange) :background (my/color-value 'orange)) (ansi-color-bright-green :foreground (my/color-value 'teal) :background (my/color-value 'teal)) (ansi-color-bright-yellow :foreground (my/color-value 'yellow) :background (my/color-value 'yellow)) (ansi-color-bright-blue :foreground (my/color-value 'blue) :background (my/color-value 'blue)) (ansi-color-bright-magenta :foreground (my/color-value 'magenta) :background (my/color-value 'magenta)) (ansi-color-bright-cyan :foreground (my/color-value 'cyan) :background (my/color-value 'cyan)) (ansi-color-bright-white :foreground (my/color-value 'fg) :background (my/color-value 'fg)))) #+end_src ** 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: - [[https://nerdfonts.com][nerd fonts homepage]] #+begin_src emacs-lisp (when (display-graphic-p) (if (x-list-fonts "JetBrainsMono Nerd Font") (let ((font "-JB -JetBrainsMono Nerd Font-medium-normal-normal-*-17-*-*-*-m-0-iso10646-1")) (set-frame-font font nil t) (add-to-list 'default-frame-alist `(font . ,font))) (message "Install JetBrainsMono Nerd Font!"))) #+end_src To make the icons work (e.g. in the Doom Modeline), run =M-x all-the-icons-install-fonts=. The package definition is somewhere later in the config. *** Other fonts #+begin_src emacs-lisp (when (display-graphic-p) (set-face-attribute 'variable-pitch nil :family "Cantarell" :height 1.0) (set-face-attribute 'italic nil :family "JetBrainsMono Nerd Font" :weight 'regular :slant 'italic)) #+end_src *** Ligatures Ligature setup for the JetBrainsMono font. #+begin_src emacs-lisp (use-package ligature :straight (:host github :repo "mickeynp/ligature.el") :if (display-graphic-p) :config (ligature-set-ligatures '( typescript-mode typescript-ts-mode js2-mode javascript-ts-mode vue-mode svelte-mode scss-mode php-mode python-mode python-ts-mode js-mode markdown-mode clojure-mode go-mode sh-mode haskell-mode web-mode) '("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<>" "<<<" ">>>" "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "" "/>" "" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" ">--" "-<" "-<<" ">->" "<-<" "<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".." ".=" ".-" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__")) (global-ligature-mode t)) #+end_src *** Icons Run =M-x all-the-icons-install-fonts= at first setup. #+begin_src emacs-lisp (use-package all-the-icons :if (display-graphic-p) :straight t) #+end_src ** Text highlight Highlight indent guides. #+begin_src emacs-lisp (use-package highlight-indent-guides :straight t :if (not (or my/remote-server)) :hook ((prog-mode . highlight-indent-guides-mode) (LaTeX-mode . highlight-indent-guides-mode)) :config (setq highlight-indent-guides-method 'bitmap) (setq highlight-indent-guides-bitmap-function 'highlight-indent-guides--bitmap-line)) #+end_src Rainbow parentheses. #+begin_src emacs-lisp (use-package rainbow-delimiters :straight t :hook ((prog-mode . rainbow-delimiters-mode))) #+end_src Highlight colors #+begin_src emacs-lisp (use-package rainbow-mode :commands (rainbow-mode) :straight t) #+end_src Highlight TODOs and stuff #+begin_src emacs-lisp (use-package hl-todo :hook (prog-mode . hl-todo-mode) :straight t) #+end_src ** Doom Modeline A modeline from Doom Emacs. A big advantage of this package is that it just works out of the box and does not require much customization. I tried a bunch of other options, including [[https://github.com/TheBB/spaceline][spaceline]], but in the end, decided that Doom Modeline works best for me. References: - [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] #+begin_src emacs-lisp (use-package doom-modeline :straight t ;; :if (not (display-graphic-p)) :init (setq doom-modeline-env-enable-python nil) (setq doom-modeline-env-enable-go nil) (setq doom-modeline-buffer-encoding 'nondefault) (setq doom-modeline-hud t) (setq doom-modeline-persp-icon nil) (setq doom-modeline-persp-name nil) (setq doom-modeline-display-misc-in-all-mode-lines nil) :config (setq doom-modeline-minor-modes nil) (setq doom-modeline-irc nil) (setq doom-modeline-buffer-state-icon nil) (doom-modeline-mode 1)) #+end_src ** perspective.el [[https://github.com/nex3/perspective-el][perspective.el]] is a package that groups buffers in "perspectives". =tab-bar.el= can be configured to behave in a similar way, but I'm too invested in this package already. One thing I don't like is that the list perspectives is displayed in the modeline, but I'll probably look how to move them to the bar at the top of the frame at some point. #+begin_src emacs-lisp (use-package perspective :straight t :init ;; (setq persp-show-modestring 'header) (setq persp-sort 'created) (setq persp-suppress-no-prefix-key-warning t) :config (persp-mode) (my-leader-def "x" '(:keymap perspective-map :which-key "perspective")) (general-define-key :keymaps 'override :states '(normal emacs) "gt" 'persp-next "gT" 'persp-prev "gn" 'persp-switch "gN" 'persp-kill) (general-define-key :keymaps 'perspective-map "b" 'persp-ivy-switch-buffer "x" 'persp-ivy-switch-buffer "u" 'persp-ibuffer)) #+end_src *** Functions to manage buffers Move the current buffer to a perspective and switch to it. #+begin_src emacs-lisp (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))) #+end_src Copy the current buffer to a perspective and switch to it. #+begin_src emacs-lisp (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))) #+end_src Switch to a perspective buffer in other window. #+begin_src emacs-lisp (defun my/persp-ivy-switch-buffer-other-window (arg) (interactive "P") (declare-function ivy-switch-buffer-other-window "ivy.el") (persp--switch-buffer-ivy-counsel-helper arg (lambda () (ivy-read "Switch to buffer in other window: " #'internal-complete-buffer :keymap ivy-switch-buffer-map :preselect (buffer-name (other-buffer (current-buffer))) :action #'ivy--switch-buffer-other-window-action :matcher #'ivy--switch-buffer-matcher :caller 'ivy-switch-buffer)))) #+end_src Add keybindings to the default map. #+begin_src emacs-lisp (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)) #+end_src *** Automating perspectives Out-of-the-box, =perspective.el= doesn't feature much (or any) capacity for automation. We're supposed to manually assign buffers to perspectives, which kinda makes sense... But I still want automation. First, let's define a variable with "rules": #+begin_src emacs-lisp (setq my/perspective-assign-alist '()) #+end_src One rule looks as follows: #+begin_example (major-mode workspace-index persp-name) #+end_example And a function to act on these rules. #+begin_src emacs-lisp (defvar my/perspective-assign-ignore nil "If non-nil, ignore `my/perspective-assign'") (defun my/perspective-assign () (when-let* ((_ (not my/perspective-assign-ignore)) (rule (alist-get major-mode my/perspective-assign-alist))) (let ((workspace-index (car rule)) (persp-name (cadr rule)) (buffer (current-buffer))) (if (fboundp #'perspective-exwm-assign-window) (progn (perspective-exwm-assign-window :workspace-index workspace-index :persp-name persp-name) (when workspace-index (exwm-workspace-switch workspace-index)) (when persp-name (persp-switch persp-name))) (with-perspective persp-name (persp-set-buffer buffer)) (persp-switch-to-buffer buffer))))) #+end_src Also advise to ignore the assignment: #+begin_src emacs-lisp (defun my/perspective-assign-ignore-advice (fun &rest args) (let ((my/perspective-assign-ignore t)) (apply fun args))) #+end_src If EXWM is available, then so is mine =perspective-exwm= package, which features a convenient procedure called =perspective-exwm-assign-window=. Otherwise, we just work with perspectives. Now, we have to put this function somewhere, and =after-change-major-mode-hook= seems like a perfect place for it. #+begin_src emacs-lisp (add-hook 'after-change-major-mode-hook #'my/perspective-assign) #+end_src And here is a simple macro to add rules to the list. #+begin_src emacs-lisp (defmacro my/persp-add-rule (&rest body) (declare (indent 0)) (unless (= (% (length body) 3) 0) (error "Malformed body in my/persp-add-rule")) (let (result) (while body (let ((major-mode (pop body)) (workspace-index (pop body)) (persp-name (pop body))) (push `(add-to-list 'my/perspective-assign-alist '(,major-mode . (,workspace-index ,persp-name))) result))) `(progn ,@result))) #+end_src Also, the logic above works only for cases when the buffer is created. Occasionally, packages run =switch-to-buffer=, which screws both EXWM workspaces and perspectives; to work around that, I define a macro that runs a command in the context of a given perspective and workspace. #+begin_src emacs-lisp (defmacro my/command-in-persp (command-name persp-name workspace-index &rest args) `'((lambda () (interactive) (when (and ,workspace-index (fboundp #'exwm-workspace-switch-create)) (exwm-workspace-switch-create ,workspace-index)) (persp-switch ,persp-name) (delete-other-windows) ,@args) :wk ,command-name)) #+end_src This is meant to be used in the definitions of =general.el=. * Programming ** General setup *** Treemacs [[https://github.com/Alexander-Miller/treemacs][Treemacs]] is a rather large & powerful package, but as of now I've replaced it with dired. I still have a small configuration because lsp-mode and dap-mode depend on it. #+begin_src emacs-lisp (use-package treemacs :straight t :defer t :config ;; (setq treemacs-follow-mode nil) ;; (setq treemacs-follow-after-init nil) (setq treemacs-space-between-root-nodes nil) ;; (treemacs-git-mode 'extended) ;; (add-to-list 'treemacs-pre-file-insert-predicates #'treemacs-is-file-git-ignored?) (general-define-key :keymaps 'treemacs-mode-map [mouse-1] #'treemacs-single-click-expand-action "M-l" #'treemacs-root-down "M-h" #'treemacs-root-up "q" #'treemacs-quit) (general-define-key :keymaps 'treemacs-mode-map :states '(normal emacs) "q" 'treemacs-quit)) (use-package treemacs-evil :after (treemacs evil) :straight t) #+end_src *** LSP LSP-mode provides an IDE-like experience for Emacs - real-time diagnostics, code actions, intelligent autocompletion, etc. References: - [[https://emacs-lsp.github.io/lsp-mode/][lsp-mode homepage]] **** Setup #+begin_src emacs-lisp (use-package lsp-mode :straight t :if (not (or my/slow-ssh my/is-termux my/remote-server)) :hook ( (typescript-mode . lsp) (js-mode . lsp) (vue-mode . lsp) (go-mode . lsp) (svelte-mode . lsp) ;; (python-mode . lsp) (json-mode . lsp) (haskell-mode . lsp) (haskell-literate-mode . lsp) (java-mode . lsp) ;; (csharp-mode . lsp) ) :commands lsp :init (setq lsp-keymap-prefix nil) :config (setq lsp-idle-delay 1) (setq lsp-eslint-server-command '("node" "/home/pavel/.emacs.d/.cache/lsp/eslint/unzipped/extension/server/out/eslintServer.js" "--stdio")) (setq lsp-eslint-run "onSave") (setq lsp-signature-render-documentation nil) ;; (lsp-headerline-breadcrumb-mode nil) (setq lsp-headerline-breadcrumb-enable nil) (setq lsp-modeline-code-actions-enable nil) (setq lsp-modeline-diagnostics-enable nil) (add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte"))) (use-package lsp-ui :straight t :commands lsp-ui-mode :config (setq lsp-ui-doc-delay 2) (setq lsp-ui-sideline-show-hover nil)) #+end_src #+RESULTS: : t **** Integrations The only integration left now is treemacs. Origami should've leveraged LSP folding, but it was too unstable at the moment I tried it. #+begin_src emacs-lisp ;; (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) #+end_src **** Keybindings #+begin_src emacs-lisp (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) #+end_src **** UI I don't like how some language servers print the full filename in the progress indicator. #+begin_src emacs-lisp (defun my/lsp--progress-status () "Returns the status of the progress for the current workspaces." (-let ((progress-status (s-join "|" (-keep (lambda (workspace) (let ((tokens (lsp--workspace-work-done-tokens workspace))) (unless (ht-empty? tokens) (mapconcat (-lambda ((&WorkDoneProgressBegin :message? :title :percentage?)) (concat (if percentage? (if (numberp percentage?) (format "%.0f%%%% " percentage?) (format "%s%%%% " percentage?)) "") (let ((msg (url-unhex-string (or message\? title)))) (if (string-match-p "\\`file:///" msg) (file-name-nondirectory msg))))) (ht-values tokens) "|")))) (lsp-workspaces))))) (unless (s-blank? progress-status) (concat lsp-progress-prefix progress-status)))) (with-eval-after-load 'lsp-mode (advice-add 'lsp--progress-status :override #'my/lsp--progress-status)) #+end_src *** Flycheck A syntax checking extension for Emacs. Integrates with LSP-mode, but can also use various standalone checkers. References: - [[https://www.flycheck.org/en/latest/][Flycheck homepage]] #+begin_src emacs-lisp (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)))) #+end_src *** General additional config Have to put this before tree-sitter because I need =my/set-smartparens-indent= there. Make smartparens behave the way I like for C-like languages. #+begin_src emacs-lisp (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")))) #+end_src Override flycheck checker with eslint. #+begin_src emacs-lisp (defun my/set-flycheck-eslint() "Override flycheck checker with eslint." (setq-local lsp-diagnostic-package :none) (setq-local flycheck-checker 'javascript-eslint)) #+end_src *** Tree Sitter Tree-Sitter integration with Emacs 29. References: - [[https://www.masteringemacs.org/article/how-to-get-started-tree-sitter][How to Get Started with Tree-Sitter - Mastering Emacs]] #+begin_src emacs-lisp (use-package treesit :straight (:type built-in) :if (featurep 'treesit) :config (setq treesit-language-source-alist (mapcar (lambda (item) (let ((lang (nth 0 item)) (url (nth 1 item)) (rev (nth 2 item)) (source-dir (nth 3 item))) `(,lang ,url ,rev ,source-dir ,(executable-find "gcc") ,(executable-find "c++")))) '((bash "https://github.com/tree-sitter/tree-sitter-bash") (cmake "https://github.com/uyha/tree-sitter-cmake") (css "https://github.com/tree-sitter/tree-sitter-css") (elisp "https://github.com/Wilfred/tree-sitter-elisp") (go "https://github.com/tree-sitter/tree-sitter-go") (html "https://github.com/tree-sitter/tree-sitter-html") (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src") (json "https://github.com/tree-sitter/tree-sitter-json") (make "https://github.com/alemuller/tree-sitter-make") (markdown "https://github.com/ikatyang/tree-sitter-markdown") (python "https://github.com/tree-sitter/tree-sitter-python") (toml "https://github.com/tree-sitter/tree-sitter-toml") (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src") (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src") (yaml "https://github.com/ikatyang/tree-sitter-yaml")))) (setq treesit-font-lock-level 4) (setq major-mode-remap-alist '((typescript-mode . typescript-ts-mode) (js-mode . javascript-ts-mode) (python-mode . python-ts-mode) (json-mode . json-ts-mode))) (cl-loop for (old-mode . new-mode) in major-mode-remap-alist do (my/set-smartparens-indent new-mode) do (set (intern (concat (symbol-name new-mode) "-hook")) (list (eval `(lambda () (run-hooks ',(intern (concat (symbol-name old-mode) "-hook"))))))))) #+end_src *** DAP An Emacs client for Debugger Adapter Protocol. Okay, so, I tried to use it many times... Chrome DevTools and ipdb / pudb are just better for me. Maybe I'll check out RealGUD instead... Will see. References: - [[https://emacs-lsp.github.io/dap-mode/][dap-mode homepage]] #+begin_src emacs-lisp (use-package dap-mode :straight t :if (not (or my/remote-server my/is-termux)) :commands (dap-debug) :init (setq lsp-enable-dap-auto-configure nil) :config (setq dap-ui-variable-length 100) (setq dap-auto-show-output nil) (require 'dap-node) (dap-node-setup) (require 'dap-chrome) (dap-chrome-setup) (require 'dap-python) (require 'dap-php) (dap-mode 1) (dap-ui-mode 1) (dap-tooltip-mode 1) (tooltip-mode 1)) #+end_src **** Controls I don't like some keybindings in the built-in hydra, and there seems to be no easy way to modify the existing hydra, so I create my own. I tried to use transient, but the transient buffer seems to conflict with special buffers of DAP, and hydra does not. Also, I want the hydra to toggle UI windows instead of just opening them, so here is a macro that defines such functions: #+begin_src emacs-lisp (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")) #+end_src And here is the hydra: #+begin_src emacs-lisp (defhydra my/dap-hydra (:color pink :hint nil :foreign-keys run) " ^Stepping^ ^UI^ ^Switch^ ^Breakpoints^ ^Debug^ ^Expressions ^^^^^^^^------------------------------------------------------------------------------------------------------------------------------------------ _n_: Next _uc_: Controls _ss_: Session _bb_: Toggle _dd_: Debug _ee_: Eval _i_: Step in _ue_: Expressions _st_: Thread _bd_: Delete _dr_: Debug recent _er_: Eval region _o_: Step out _ul_: Locals _sf_: Stack frame _ba_: Add _dl_: Debug last _es_: Eval thing at point _c_: Continue _ur_: REPL _su_: Up stack frame _bc_: Set condition _de_: Edit debug template _ea_: Add expression _r_: Restart frame _uo_: Output _sd_: Down stack frame _bh_: Set hit count _Q_: Disconnect _ed_: Remove expression _us_: Sessions _sF_: Stack frame filtered _bl_: Set log message _eu_: Refresh expressions _ub_: Breakpoints " ("n" dap-next) ("i" dap-step-in) ("o" dap-step-out) ("c" dap-continue) ("r" dap-restart-frame) ("uc" dap-ui-controls-mode) ("ue" my/dap-ui-toggle-expressions) ("ul" my/dap-ui-toggle-locals) ("ur" my/dap-ui-toggle-repl) ("uo" dap-go-to-output-buffer) ("us" my/dap-ui-toggle-sessions) ("ub" my/dap-ui-toggle-breakpoints) ("ss" dap-switch-session) ("st" dap-switch-thread) ("sf" dap-switch-stack-frame) ("sF" my/dap-switch-stack-frame) ("su" dap-up-stack-frame) ("sd" dap-down-stack-frame) ("bb" dap-breakpoint-toggle) ("ba" dap-breakpoint-add) ("bd" dap-breakpoint-delete) ("bc" dap-breakpoint-condition) ("bh" dap-breakpoint-hit-condition) ("bl" dap-breakpoint-log-message) ("dd" dap-debug) ("dr" dap-debug-recent) ("dl" dap-debug-last) ("de" dap-debug-edit-template) ("ee" dap-eval) ("ea" dap-ui-expressions-add) ("er" dap-eval-region) ("es" dap-eval-thing-at-point) ("ed" dap-ui-expressions-remove) ("eu" dap-ui-expressions-refresh) ("q" nil "quit" :color blue) ("Q" dap-disconnect :color red)) (my-leader-def "d" #'my/dap-hydra/body) #+end_src **** UI Fixes There are some problems with DAP UI in my setup. First, DAP uses Treemacs buffers quite extensively, and they hide the doom modeline for some reason, so I can't tell which buffer is active and can't see borders between buffers. Second, lines are truncated in some strange way, but calling =toggle-truncate-lines= seems to fix that. So I define a macro that creates a function that I can further use in advices. #+begin_src emacs-lisp (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)) #+end_src **** 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. #+begin_src emacs-lisp (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))) #+end_src A function to kill a value from a treemacs node. #+begin_src emacs-lisp (defun my/dap-yank-value-at-point (node) (interactive (list (treemacs-node-at-point))) (kill-new (message (plist-get (button-get node :item) :value)))) #+end_src A function to open a value from a treemacs node in a new buffer. #+begin_src emacs-lisp (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)))))) #+end_src **** Switch to stack frame with filter One significant improvement over Chrome Inspector for my particular stack is an ability to filter the stack frame list, for instance, to see only frames that relate to my current project. So, here are functions that customize the filters: #+begin_src emacs-lisp (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))))) #+end_src And here is a version of =dap-switch-stack-frame= that uses the said filter. #+begin_src emacs-lisp (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))))) #+end_src **** Smarter switch to stack frame - *CREDIT*: Thanks @yyoncho on the Emacs LSP Discord for helping me with this! By default, when a breakpoint is hit, dap always pop us the buffer in the active EXWM workspace and in the active perspective. I'd like it to switch to an existing buffer instead. So first we need to locate EXWM workspace for the file with =path=: #+begin_src emacs-lisp (defun my/exwm-perspective-find-buffer (path) "Find a buffer with PATH in all EXWM perspectives. Returns ( . ) or nil." (let* ((buf (cl-loop for buf being buffers if (and (buffer-file-name buf) (f-equal-p (buffer-file-name buf) path)) return buf)) (target-workspace (and buf (cl-loop for frame in exwm-workspace--list if (with-selected-frame frame (cl-loop for persp-name being the hash-keys of (perspectives-hash) if (member buf (persp-buffers (gethash persp-name (perspectives-hash)))) return persp-name)) return (cl-position frame exwm-workspace--list))))) (when target-workspace (cons buf target-workspace)))) #+end_src And override =dap--go-to-stack-frame= to take that into account. For some reason, evaluating this before =dap-mode= doesn't work. #+begin_src emacs-lisp (defun my/dap--go-to-stack-frame-override (debug-session stack-frame) "Make STACK-FRAME the active STACK-FRAME of DEBUG-SESSION." (with-lsp-workspace (dap--debug-session-workspace debug-session) (when stack-frame (-let* (((&hash "line" line "column" column "name" name) stack-frame) (path (dap--get-path-for-frame stack-frame))) (setf (dap--debug-session-active-frame debug-session) stack-frame) ;; If we have a source file with path attached, open it and ;; position the point in the line/column referenced in the ;; stack trace. (if (and path (file-exists-p path)) (progn (let ((exwm-target (my/exwm-perspective-find-buffer path))) (if exwm-target (progn (unless (= (cdr exwm-target) exwm-workspace-current-index) (exwm-workspace-switch (cdr exwm-target))) (persp-switch-to-buffer (car exwm-target))) (select-window (get-mru-window (selected-frame) nil)) (find-file path))) (goto-char (point-min)) (forward-line (1- line)) (forward-char column)) (message "No source code for %s. Cursor at %s:%s." name line column)))) (run-hook-with-args 'dap-stack-frame-changed-hook debug-session))) (with-eval-after-load 'exwm (with-eval-after-load 'dap-mode (advice-add #'dap--go-to-stack-frame :override #'my/dap--go-to-stack-frame-override))) ;; (advice-remove #'dap--go-to-stack-frame #'my/dap--go-to-stack-frame-override) #+end_src **** Debug templates Some debug templates I frequently use. #+begin_src emacs-lisp (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"))) #+end_src *** Reformatter A general-purpose package to run formatters on files. While the most popular formatters are already packaged for Emacs, those that aren't can be invoked with this package. #+begin_src emacs-lisp (use-package reformatter :straight t) #+end_src *** copilot [[https://copilot.github.com/][GitHub Copilot]] is a project of GitHub and OpenAI that provides code completions. It's somewhat controversial in the Emacs community but I opt in for now. #+begin_src emacs-lisp (defun my/copilot-tab () (interactive) (or (copilot-accept-completion) (when (my/should-run-emmet-p) (my/emmet-or-tab)) (when (and (eq evil-state 'normal) (or hs-minor-mode outline-minor-mode)) (evil-toggle-fold) t) (indent-for-tab-command))) (use-package copilot :straight (:host github :repo "SqrtMinusOne/copilot.el" :files ("dist" "*.el")) :commands (copilot-mode) :if (not (or my/remote-server my/is-termux)) :init (add-hook 'prog-mode-hook #'copilot-mode) :config (setq copilot-node-executable "/home/pavel/.guix-extra-profiles/dev/dev/bin/node") (general-define-key :keymaps 'company-active-map "" #'my/copilot-tab) (general-define-key :keymaps 'copilot-mode-map "" #'my/copilot-tab "M-j" #'copilot-accept-completion-by-line "M-l" #'copilot-accept-completion-by-word) (setq copilot-lispy-integration t)) #+end_src ** Web development Configs for various web development technologies I'm using. *** Emmet [[https://emmet.io/][Emmet]] is a toolkit which greatly speeds up typing HTML & CSS. | Type | Note | |------+---------------------------------------------------| | TODO | make expand div[disabled] as
| My bit of config here: - makes =TAB= the only key I have to use #+begin_src emacs-lisp (defun my/should-run-emmet-p () (and (bound-and-true-p emmet-mode) (or (and (derived-mode-p 'web-mode) (member (web-mode-language-at-pos) '("html" "css"))) (not (derived-mode-p 'web-mode))))) (use-package emmet-mode :straight t :hook ((vue-html-mode . emmet-mode) (svelte-mode . emmet-mode) (web-mode . emmet-mode) (html-mode . emmet-mode) (css-mode . emmet-mode) (scss-mode . emmet-mode)) :config (defun my/emmet-or-tab (&optional arg) (interactive) (if (my/should-run-emmet-p) (or (emmet-expand-line arg) (emmet-go-to-edit-point 1) (indent-for-tab-command arg)) (indent-for-tab-command arg))) (general-imap :keymaps 'emmet-mode-keymap "TAB" 'my/emmet-or-tab "" 'emmet-prev-edit-point)) #+end_src *** Prettier #+begin_src emacs-lisp (use-package prettier :commands (prettier-prettify) :straight t :init (my-leader-def :keymaps '(js-mode-map web-mode-map typescript-mode-map typescript-ts-mode-map vue-mode-map svelte-mode-map) "rr" #'prettier-prettify)) #+end_src *** TypeScript #+begin_src emacs-lisp (use-package typescript-mode :straight t :mode "\\.ts\\'" :init (add-hook 'typescript-mode-hook #'smartparens-mode) (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode) (add-hook 'typescript-mode-hook #'hs-minor-mode) :config (my/set-smartparens-indent 'typescript-mode)) #+end_src *** JavaScript #+begin_src emacs-lisp (add-hook 'js-mode-hook #'smartparens-mode) (add-hook 'js-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'js-mode) #+end_src *** Jest #+begin_src emacs-lisp (use-package jest-test-mode :straight t :hook ((typescript-mode . jest-test-mode) (js-mode . jest-test-mode)) :config (my-leader-def :keymaps 'jest-test-mode-map :infix "t" "t" #'jest-test-run-at-point "d" #'jest-test-debug-run-at-point "r" #'jest-test-run "a" #'jest-test-run-all-tests) (defmacro my/jest-test-with-debug-flags (form) "Execute FORM with debugger flags set." (declare (indent 0)) `(let ((jest-test-options (seq-concatenate 'list jest-test-options (list "--runInBand") )) (jest-test-npx-options (seq-concatenate 'list jest-test-npx-options (list "--node-options" "--inspect-brk")))) ,form)) (defun my/jest-test-debug () "Run the test with an inline debugger attached." (interactive) (my/jest-test-with-debug-flags (jest-test-run))) (defun my/jest-test-debug-rerun-test () "Run the test with an inline debugger attached." (interactive) (my/jest-test-with-debug-flags (jest-test-rerun-test))) (defun my/jest-test-debug-run-at-point () "Run the test with an inline debugger attached." (interactive) (my/jest-test-with-debug-flags (jest-test-run-at-point))) (advice-add #'jest-test-debug :override #'my/jest-test-debug) (advice-add #'jest-test-debug-rerun-test :override #'my/jest-test-debug-rerun-test) (advice-add #'jest-test-debug-run-at-point :override #'my/jest-test-debug-run-at-point)) #+end_src #+begin_src emacs-lisp (defun my/jest-test-run-at-point-copy () "Run the top level describe block of the current buffer's point." (interactive) (let ((filename (jest-test-find-file)) (example (jest-test-unit-at-point))) (if (and filename example) (jest-test-from-project-directory filename (let ((jest-test-options (seq-concatenate 'list jest-test-options (list "-t" example)))) (kill-new (jest-test-command filename)))) (message jest-test-not-found-message)))) #+end_src *** web-mode [[https://web-mode.org/][web-mode.el]] is a major mode to edit various web templates. Trying this one out instead of vue-mode and svelte-mode, because this one seems to have better support for tree-sitter and generally less problems. Set =web-mode-auto-pairs= not =nil= because smartparens already fulfills that role. #+begin_src emacs-lisp (use-package web-mode :straight t :commands (web-mode) :init (add-to-list 'auto-mode-alist '("\\.svelte\\'" . web-mode)) (add-to-list 'auto-mode-alist '("\\.vue\\'" . web-mode)) :config (add-hook 'web-mode-hook 'smartparens-mode) (add-hook 'web-mode-hook 'hs-minor-mode) (my/set-smartparens-indent 'web-mode) (setq web-mode-auto-pairs nil)) #+end_src Hooking this up with lsp. #+begin_src emacs-lisp (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) #+end_src Vue settings #+begin_src emacs-lisp (defun my/web-mode-vue-setup (&rest _) (when (string-match-p (rx ".vue" eos) (buffer-name)) (setq-local web-mode-script-padding 0) (setq-local web-mode-style-padding 0) (setq-local create-lockfiles nil) (setq-local web-mode-enable-auto-pairing nil))) (add-hook 'web-mode-hook 'my/web-mode-vue-setup) (add-hook 'editorconfig-after-apply-functions 'my/web-mode-vue-setup) #+end_src *** SCSS #+begin_src emacs-lisp (add-hook 'scss-mode-hook #'smartparens-mode) (add-hook 'scss-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'scss-mode) #+end_src *** PHP #+begin_src emacs-lisp (use-package php-mode :straight t :mode "\\.php\\'" :config (add-hook 'php-mode-hook #'smartparens-mode) (add-hook 'php-mode-hook #'lsp) (my/set-smartparens-indent 'php-mode)) #+end_src ** LaTeX *** AUCTeX The best LaTeX editing environment I've found so far. References: - [[https://www.gnu.org/software/auctex/][AUCTeX homepage]] #+begin_src emacs-lisp :noweb yes (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) <> <> <> <>) #+end_src *** Import *.sty A function to import =.sty= files to the LaTeX document. #+begin_src emacs-lisp (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))))) #+end_src *** 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. #+begin_src emacs-lisp :noweb-ref init-greek-latex-snippets (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)) #+end_src **** English letters #+begin_src emacs-lisp :noweb-ref init-english-latex-snippets (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)) #+end_src **** Math symbols #+begin_src emacs-lisp :noweb-ref init-math-latex-snippets (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)) #+end_src **** Section snippets Section snippets. The code turned out to be more complicated than just writing the snippets by hand. #+begin_src emacs-lisp :noweb-ref init-section-latex-snippets (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) #+end_src ** Markup & natural languages *** Markdown #+begin_src emacs-lisp (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-" 'markdown-promote "M-" 'markdown-demote)) ;; (use-package livedown ;; :straight (:host github :repo "shime/emacs-livedown") ;; :commands livedown-preview ;; :config ;; (setq livedown-browser "qutebrowser")) #+end_src *** Ascii Doc #+begin_src emacs-lisp (use-package adoc-mode :straight t) #+end_src *** PlantUML | Guix dependency | |-----------------| | plantuml | #+begin_src emacs-lisp (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)) #+end_src *** Subtitles A major mode to work with subtitles. #+begin_src emacs-lisp (use-package subed :straight (:host github :repo "rndusr/subed" :files ("subed/*.el") :build (:not native-compile)) :config (general-define-key :keymaps '(subed-mode-map subed-vtt-mode-map) :states '(normal) "gp" #'subed-mpv-toggle-pause)) #+end_src *** LTeX [[https://github.com/valentjn/ltex-ls][ltex-ls]] is a tool that wraps LanguageTool into a language server. It takes maybe 10 seconds to run on my Master's thesis file (=M-x count words=: 13453 words and 117566 characters), but it's totally worth it. And it's much faster on smaller files. The good thing is that it supports markup syntaxes like Org and Markdown, whereas LanguageTool by itself produces a lot of false positives on these files. It shouldn't be too hard to package that for guix, but I've installed the nix version for now. #+begin_src emacs-lisp (use-package lsp-ltex :straight t :after (lsp) :init (setq lsp-ltex-version "15.2.0") (setq lsp-ltex-check-frequency "save")) #+end_src A function to switch the current language. #+begin_src emacs-lisp (defun my/ltex-lang () (interactive) (setq lsp-ltex-language (completing-read "Language: " '("en-US" "ru-RU" "de-DE"))) (lsp-workspace-restart (lsp--read-workspace))) #+end_src Check whether it's necessary to run LTeX: #+begin_src emacs-lisp (defun my/ltex-need-p () (let ((file-name (buffer-file-name))) (cond ((null file-name) nil) ((string-match-p (rx "/home/pavel/" (+ alnum) ".org" eos) file-name) nil) ((string-match-p (rx (literal org-directory) "/" (or "roam" "inbox-notes" "literature-notes" "journal")) file-name) t) ((string-match-p (rx (literal org-directory)) file-name) nil) ((string-match-p (rx (literal (expand-file-name user-emacs-directory))) file-name) nil) (t t)))) #+end_src To use it in =text-mode-hook= #+begin_src emacs-lisp (defun my/text-mode-lsp-maybe () (when (my/ltex-need-p) (lsp))) (add-hook 'text-mode-hook #'my/text-mode-lsp-maybe) #+end_src *** 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: - [[https://languagetool.org/][LanguageTool homepage]] - [[https://dev.languagetool.org/http-server][LanguageTool http server]] #+begin_src emacs-lisp (use-package langtool :straight t :commands (langtool-check) :config (setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-5.7/languagetool-server.jar") (setq langtool-mother-tongue "ru") (setq langtool-default-language "en-US")) (my-leader-def :infix "L" "" '(:which-key "languagetool") "c" 'langtool-check "s" 'langtool-server-stop "d" 'langtool-check-done "n" 'langtool-goto-next-error "p" 'langtool-goto-previous-error "l" 'langtool-correct-buffer) #+end_src *** Reverso [[https://github.com/SqrtMinusOne/reverso.el][reverso.el]] is a package of mine that provides Emacs interface for [[https://reverso.net]]. #+begin_src emacs-lisp (use-package reverso :straight (:host github :repo "SqrtMinusOne/reverso.el") :init (my-leader-def "ar" #'reverso) :config (setq reverso-languages '(russian english german)) (reverso-history-mode)) #+end_src ** Lisp [[file:dot-imgs/lisp_cycles.png]] *** Meta Lisp Some packages for editing various Lisps. #+begin_src emacs-lisp (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)) #+end_src *** Emacs Lisp **** Package Lint A package that checks for the metadata in Emacs Lisp packages. #+begin_src emacs-lisp (use-package flycheck-package :straight t :after flycheck :config (flycheck-package-setup)) #+end_src **** General settings #+begin_src emacs-lisp (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) #+end_src **** Helper functions Remove all advice from function. Source: https://emacs.stackexchange.com/questions/24657/unadvise-a-function-remove-all-advice-from-it #+begin_src emacs-lisp (defun advice-unadvice (sym) "Remove all advices from symbol SYM." (interactive "aFunction symbol: ") (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym)) #+end_src **** IELM #+begin_src emacs-lisp (add-hook 'inferior-emacs-lisp-mode-hook #'smartparens-mode) (my-leader-def "bi" #'ielm) #+end_src *** Common lisp **** SLIME #+begin_src emacs-lisp (use-package slime :straight t :commands (slime) :config (setq inferior-lisp-program "sbcl") (add-hook 'slime-repl-mode 'smartparens-mode)) #+end_src **** General settings #+begin_src emacs-lisp (add-hook 'lisp-mode-hook #'aggressive-indent-mode) ;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode) (add-hook 'lisp-mode-hook #'lispy-mode) #+end_src *** Clojure #+begin_src emacs-lisp (use-package clojure-mode :straight t :mode "\\.clj[sc]?\\'" :config ;; (add-hook 'clojure-mode-hook #'smartparens-strict-mode) (add-hook 'clojure-mode-hook #'lispy-mode) (add-hook 'clojure-mode-hook #'aggressive-indent-mode)) (use-package cider :after clojure-mode :straight t) #+end_src *** Hy Python requirements: - =hy= - =jedhy= #+begin_src emacs-lisp (use-package hy-mode :straight t :mode "\\.hy\\'" :config (add-hook 'hy-mode-hook #'lispy-mode) (add-hook 'hy-mode-hook #'aggressive-indent-mode)) #+end_src *** Scheme #+begin_src emacs-lisp (use-package geiser :straight t :commands (geiser run-geiser) :config (setq geiser-default-implementation 'guile)) (use-package geiser-guile :straight t :after geiser) (add-hook 'scheme-mode-hook #'aggressive-indent-mode) (add-hook 'scheme-mode-hook #'lispy-mode) #+end_src *** CLIPS An honorary Lisp. #+begin_src emacs-lisp (use-package clips-mode :straight t :mode "\\.cl\\'" :disabled t :config (add-hook 'clips-mode 'lispy-mode)) #+end_src ** Python *** ein [[https://github.com/millejoh/emacs-ipython-notebook][ein]] is a package that allows for running Jupyter notebooks in Emacs. #+begin_src emacs-lisp (use-package ein :straight t) #+end_src *** pyright For some reason it doesn't use pipenv python executable, so here is a small workaround. #+begin_src emacs-lisp (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) #+end_src *** pipenv [[https://github.com/pypa/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=). #+begin_src emacs-lisp (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)) #+end_src *** OFF (OFF) yapf [[https://github.com/google/yapf][yapf]] is a formatter for Python files. | Guix dependency | |-----------------| | python-yapf | References: - [[https://github.com/google/yapf][yapf repo]] - [[https://github.com/JorisE/yapfify][yapfify.el repo]] #+begin_src emacs-lisp (use-package yapfify :straight (:repo "JorisE/yapfify" :host github) :disabled :commands (yapfify-region yapfify-buffer yapfify-region-or-buffer yapf-mode)) #+end_src Global config: #+begin_src conf-windows :tangle .config/yapf/style :comments link [style] based_on_style = facebook column_limit = 80 #+end_src *** black [[https://github.com/psf/black][black]] is a formatter for Python files. | Guix dependency | |-----------------| | python-black | #+begin_src emacs-lisp (use-package python-black :straight t :commands (python-black-buffer) :config (setq python-black-command "black")) #+end_src *** isort [[https://github.com/PyCQA/isort][isort]] is a Python package to sort Python imports. | Guix dependency | |-----------------| | python-isort | References: - [[https://pycqa.github.io/isort/][isort docs]] - [[https://github.com/paetzke/py-isort.el][py-isort.el repo]] #+begin_src emacs-lisp (use-package py-isort :straight t :commands (py-isort-buffer py-isort-region)) #+end_src The following binding calls yapf & isort on the buffer #+begin_src emacs-lisp (my-leader-def :keymaps '(python-mode-map python-ts-mode-map) "rr" (lambda () (interactive) (save-excursion (unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p)) (py-isort-buffer)) (python-black-buffer)))) #+end_src *** sphinx-doc A package to generate sphinx-compatible docstrings. #+begin_src emacs-lisp (use-package sphinx-doc :straight t :hook (python-mode . sphinx-doc-mode) :config (my-leader-def :keymaps 'sphinx-doc-mode-map "rd" 'sphinx-doc)) #+end_src *** pytest [[https://docs.pytest.org/en/6.2.x/][pytest]] is a unit testing framework for Python. Once again a function to set pytest executable from pipenv. References: - [[https://docs.pytest.org/en/6.2.x/][pytest docs]] - [[https://github.com/wbolster/emacs-python-pytest][emacs-python-pytest]] #+begin_src emacs-lisp :noweb yes (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 <> (add-hook 'python-mode-hook #'my/set-pipenv-pytest) (when (derived-mode-p 'python-mode) (my/set-pipenv-pytest))) #+end_src **** Fix comint buffer width For some reason, the default comint output width is way too large. To fix that, I've modified the following function in the =python-pytest= package. #+begin_src emacs-lisp :noweb-ref override-pytest-run :tangle no (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)))) #+end_src *** code-cells Support for text with magic comments. | Guix dependency | Disabled | |-----------------+----------| | python-jupytext | t | #+begin_src emacs-lisp (use-package code-cells :straight t :commands (code-cells-mode code-cells-convert-ipynb)) #+end_src *** tensorboard A function to start up [[https://www.tensorflow.org/tensorboard][TensorBoard]]. #+begin_src emacs-lisp (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)) #+end_src ** Data serialization *** JSON #+begin_src emacs-lisp (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)) #+end_src *** CSV #+begin_src emacs-lisp (use-package csv-mode :straight t :disabled :mode "\\.csv\\'") #+end_src *** YAML #+begin_src emacs-lisp (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))) #+end_src ** Configuration *** .env #+begin_src emacs-lisp (use-package dotenv-mode :straight t :mode "\\.env\\..*\\'") #+end_src *** .gitignore A package to quickly create =.gitignore= files. #+begin_src emacs-lisp (use-package gitignore-templates :straight t :commands (gitignore-templates-insert gitignore-templates-new-file)) #+end_src *** Docker #+begin_src emacs-lisp (use-package dockerfile-mode :mode "Dockerfile\\'" :straight t :config (add-hook 'dockerfile-mode 'smartparens-mode)) #+end_src *** Jenkins #+begin_src emacs-lisp (use-package jenkinsfile-mode :straight t :config (add-hook 'jenkinsfile-mode-hook #'smartparens-mode) (my/set-smartparens-indent 'jenkinsfile-mode)) #+end_src *** crontab #+begin_src emacs-lisp (use-package crontab-mode :straight t) #+end_src *** nginx #+begin_src emacs-lisp (use-package nginx-mode :straight t :config (my/set-smartparens-indent 'nginx-mode)) #+end_src *** HCL #+begin_src emacs-lisp (use-package hcl-mode :straight t) #+end_src ** Shell *** sh #+begin_src emacs-lisp (add-hook 'sh-mode-hook #'smartparens-mode) #+end_src *** fish #+begin_src emacs-lisp (use-package fish-mode :straight t :mode "\\.fish\\'" :config (add-hook 'fish-mode-hook #'smartparens-mode)) #+end_src ** Query languages *** SQL [[https://github.com/zeroturnaround/sql-formatter][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 [[https://github.com/purcell/emacs-reformatter][reformatter]]. Also, I've made a simple function to switch dialects because I often alternate between them. So far I didn't find a nice SQL client for Emacs, but I occasionally run SQL queries in Org Mode, so this quite package is handy. #+begin_src emacs-lisp (setq my/sqlformatter-dialect-choice '("db2" "mariadb" "mysql" "n1ql" "plsql" "postgresql" "redshift" "spark" "sql" "tsql")) (setq my/sqlformatter-dialect "postgresql") (defun my/sqlformatter-set-dialect () "Set dialect for sql-formatter" (interactive) (setq my/sqlformatter-dialect (completing-read "Dialect: " my/sqlformatter-dialect-choice))) (reformatter-define sqlformat :program (executable-find "sql-formatter") :args `("-l" ,my/sqlformatter-dialect)) (my-leader-def :keymaps '(sql-mode-map) "rr" #'sqlformat-buffer) #+end_src *** SPARQL #+begin_src emacs-lisp (use-package sparql-mode :straight t) #+end_src *** GraphQL #+begin_src emacs-lisp (use-package graphql-mode :straight t) #+end_src ** Documents *** DocView Don't know about this. =doc-view= doesn't look great with the default =doc-view-resolution= of 100. 300 is fine, but then it becomes slow. #+begin_src emacs-lisp (defun my/doc-view-setup () (display-line-numbers-mode -1) (undo-tree-mode -1)) (use-package doc-view :straight (:type built-in) :config (setq doc-view-resolution 300) (add-hook 'doc-view-mode-hook #'my/doc-view-setup) (general-define-key :states '(normal) :keymaps '(doc-view-mode-map) "j" #'doc-view-next-line-or-next-page "k" #'doc-view-previous-line-or-previous-page)) #+end_src ** x509 #+begin_src emacs-lisp (use-package x509-mode :straight t) #+end_src ** Java #+begin_src emacs-lisp (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) #+end_src ** Go #+begin_src emacs-lisp (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)) #+end_src ** .NET *** C# | Guix dependencies | Disabled | |-------------------+----------| | omnisharp | t | | dotnet | t | Disabled that for now because it depends on the old tree sitter. #+begin_src emacs-lisp (use-package csharp-mode :straight t :mode "\\.cs\\'" :disabled t :config (setq lsp-csharp-server-path (executable-find "omnisharp-wrapper")) (add-hook 'csharp-mode-hook #'csharp-tree-sitter-mode) (add-hook 'csharp-tree-sitter-mode-hook #'smartparens-mode) (add-hook 'csharp-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'csharp-tree-sitter-mode)) #+end_src *** MSBuild #+begin_src emacs-lisp (use-package csproj-mode :straight t :mode "\\.csproj\\'" :config (add-hook 'csproj-mode #'smartparens-mode)) #+end_src ** Haskell #+begin_src emacs-lisp (use-package haskell-mode :straight t :mode "\\.hs\\'") (use-package lsp-haskell :straight t :after (lsp haskell-mode)) #+end_src ** nix #+begin_src emacs-lisp (use-package nix-mode :straight t :mode "\\.nix\\'" :config (add-hook 'nix-mode-hook #'smartparens-mode) (my/set-smartparens-indent 'nix-mode)) #+end_src ** Lua #+begin_src emacs-lisp (use-package lua-mode :straight t :mode "\\.lua\\'" :hook (lua-mode . smartparens-mode)) (my/set-smartparens-indent 'lua-mode) #+end_src * Org Mode *Org mode* is a tool that leverages plain-text files for tasks like making notes, literate programming, task management, etc. References: - [[https://orgmode.org/][Org Mode homepage]] - [[https://orgmode.org/manual/][Manual]] ** Installation & basic settings Use the built-in org mode (=:type built-in=). #+begin_src emacs-lisp :noweb yes (use-package org :straight (:type built-in) :if (not my/remote-server) :defer t :init (setq org-directory (expand-file-name "~/30-39 Life/32 org-mode")) (unless (file-exists-p org-directory) (mkdir org-directory t)) :config (setq org-startup-indented (not my/is-termux)) (setq org-return-follows-link t) (setq org-src-tab-acts-natively nil) (add-hook 'org-mode-hook 'smartparens-mode) (add-hook 'org-agenda-mode-hook (lambda () (visual-line-mode -1) (toggle-truncate-lines 1) (display-line-numbers-mode 0))) (add-hook 'org-mode-hook (lambda () (rainbow-delimiters-mode -1)))) #+end_src *** Encryption Setting up =org-crypt= to encrypt parts of file. #+begin_src emacs-lisp :noweb-ref (with-eval-after-load-norem 'org (require 'org-crypt) (org-crypt-use-before-save-magic) (setq org-tags-exclude-from-inheritance '("crypt")) (setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205")) #+end_src #+begin_src emacs-lisp (with-eval-after-load 'epg (setq epg-gpg-program "gpg") (setq epg-config--program-alist `((OpenPGP epg-gpg-program ;; ("gpg2" . ,epg-gpg2-minimum-version) ("gpg" . ((,epg-gpg-minimum-version . "2.0") ,epg-gpg2-minimum-version))) (CMS epg-gpgsm-program ("gpgsm" . "2.0.4"))))) #+end_src This enables encryption for Org segments tagged =:crypt:=. Another way to encrypt Org files is to save them with the extension =.org.gpg=. However, by default [[https://www.gnu.org/software/emacs/manual/html_mono/epa.html][EPA]] always prompts for the key, which is not what I want when there is only one key to select. Hence the following advice: #+begin_src emacs-lisp (defun my/epa--select-keys-around (fun prompt keys) (if (= (seq-length keys) 1) keys (funcall fun prompt keys))) (with-eval-after-load-norem 'epa (advice-add #'epa--select-keys :around #'my/epa--select-keys-around)) (unless my/remote-server (setq epa-file-encrypt-to '("DE004F32AFA00205"))) #+end_src *** org-contrib =org-contrib= is a package with various additions to Org. I use the following: - =ox-extra= - extensions for org export This used to have =org-contacts= and =ol-notmuch= at some point, but they have since been migrated to separate repos. #+begin_src emacs-lisp (use-package org-contrib :straight (org-contrib :type git :repo "https://git.sr.ht/~bzg/org-contrib" :build t) :after (org) :if (not my/remote-server) :config (require 'ox-extra) (ox-extras-activate '(latex-header-blocks ignore-headlines))) #+end_src *** ol-notmuch [[https://git.sr.ht/~tarsius/ol-notmuch][ol-notmuch]] is a package that adds Org links to notmuch messages. #+begin_src emacs-lisp (unless (or my/remote-server my/is-termux) (use-package ol-notmuch :straight t :after (org notmuch))) #+end_src *** org-tempo =org-tempo= is a convinient package that provides snippets for various org blocks. #+begin_src emacs-lisp (with-eval-after-load 'org (require 'org-tempo) (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")) (add-to-list 'org-structure-template-alist '("py" . "src python")) (add-to-list 'org-structure-template-alist '("sq" . "src sql"))) #+end_src *** evil-org Better integration with evil-mode. #+begin_src emacs-lisp (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)) #+end_src *** Support for relative URLs Source: https://emacs.stackexchange.com/questions/9807/org-mode-dont-change-relative-urls #+begin_src emacs-lisp (defun my/export-rel-url (path desc format) (cl-case format (html (format "%s" path (or desc path))) (latex (format "\\href{%s}{%s}" path (or desc path))) (otherwise path))) (with-eval-after-load 'org (org-link-set-parameters "rel" :follow #'browse-url :export #'my/export-rel-url)) #+end_src ** Keybindings & stuff I've moved this block above because the =my-leader-def= expression in the next block seems to override the previous ones. So it has to be on the top. *** General keybindings #+begin_src emacs-lisp (with-eval-after-load-norem 'org (general-define-key :keymaps 'org-mode-map "C-c d" 'org-decrypt-entry "C-c e" 'org-encrypt-entry "M-p" 'org-latex-preview "M-o" 'org-redisplay-inline-images) (general-define-key :keymaps 'org-mode-map :states '(normal emacs) "L" 'org-shiftright "H" 'org-shiftleft "S-" 'org-next-visible-heading "S-" 'org-previous-visible-heading "M-0" 'org-next-visible-heading "M-9" 'org-previous-visible-heading "M-]" 'org-babel-next-src-block "M-[" 'org-babel-previous-src-block) (general-define-key :keymaps 'org-agenda-mode-map "M-]" 'org-agenda-later "M-[" 'org-agenda-earlier) (general-nmap :keymaps 'org-mode-map "RET" 'org-ctrl-c-ctrl-c)) #+end_src *** Copy a link #+begin_src emacs-lisp (defun my/org-link-copy (&optional arg) "Extract URL from org-mode link and add it to kill ring." (interactive "P") (let* ((link (org-element-lineage (org-element-context) '(link) t)) (type (org-element-property :type link)) (url (org-element-property :path link)) (url (concat type ":" url))) (kill-new url) (message (concat "Copied URL: " url)))) (with-eval-after-load-norem 'org (general-nmap :keymaps 'org-mode-map "C-x C-l" 'my/org-link-copy)) #+end_src *** Navigating source blocks An idea born from discussing Org Mode navigation with @Infu. Modifying =org-babel-next-src-block= and =org-babel-previous-src-block= to ignore hidden source blocks. #+begin_src emacs-lisp (defun my/org-babel-next-visible-src-block (arg) "Move to the next visible source block. With ARG, repeats or can move backward if negative." (interactive "p") (let ((regexp org-babel-src-block-regexp)) (if (< arg 0) (beginning-of-line) (end-of-line)) (while (and (< arg 0) (re-search-backward regexp nil :move)) (unless (bobp) (while (pcase (get-char-property-and-overlay (point) 'invisible) (`(outline . ,o) (goto-char (overlay-start o)) (re-search-backward regexp nil :move)) (_ nil)))) (cl-incf arg)) (while (and (> arg 0) (re-search-forward regexp nil t)) (while (pcase (get-char-property-and-overlay (point) 'invisible) (`(outline . ,o) (goto-char (overlay-end o)) (re-search-forward regexp nil :move)) (_ (end-of-line) nil))) (re-search-backward regexp nil :move) (cl-decf arg)) (if (> arg 0) (goto-char (point-max)) (beginning-of-line)))) (defun my/org-babel-previous-visible-src-block (arg) "Move to the prevous visible source block. With ARG, repeats or can move backward if negative." (interactive "p") (my/org-babel-next-visible-src-block (- arg))) (with-eval-after-load 'org (general-define-key :keymaps 'org-mode-map :states '(normal emacs) "M-]" #'my/org-babel-next-visible-src-block "M-[" #'my/org-babel-previous-visible-src-block)) #+end_src *** Open a file from =org-directory= A function to open a file from =org-directory=, excluding a few directories like =roam= and =journal=. #+begin_src emacs-lisp (defun my/org-file-open () (interactive) (let* ((files (thread-last '("projects" "misc") (mapcar (lambda (f) (directory-files (concat org-directory "/" f) t (rx ".org" eos)))) (apply #'append) (mapcar (lambda (file) (string-replace (concat org-directory "/") "" file))) (append '("inbox.org" "contacts.org"))))) (find-file (concat org-directory "/" (completing-read "Org file: " files))))) #+end_src ** Literate programing *** Python & Jupyter Use jupyter kernels for Org Mode. References: - [[https://github.com/nnicandro/emacs-jupyter][emacs-jupyter repo]] - [[https://github.com/jkitchin/scimax/blob/master/scimax.org][SCIMAX manual]] #+begin_src emacs-lisp (use-package jupyter :straight t :after (org) :if (not (or my/remote-server my/is-termux))) #+end_src 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). #+begin_src emacs-lisp (defun my/jupyter-refresh-kernelspecs () "Refresh Jupyter kernelspecs" (interactive) (jupyter-available-kernelspecs t)) #+end_src 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. #+begin_src emacs-lisp (defun my/jupyter-refesh-langs () "Refresh Jupyter languages" (interactive) (org-babel-jupyter-aliases-from-kernelspecs t)) #+end_src *** Hy #+begin_src emacs-lisp (use-package ob-hy :after (org) :if (not my/remote-server) :straight t) #+end_src *** View HTML in browser Open HTML in the ~begin_export~ block with xdg-open. #+begin_src emacs-lisp (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)))))) #+end_src *** PlantUML #+begin_src emacs-lisp (with-eval-after-load 'org (setq org-plantuml-executable-path "/home/pavel/.guix-extra-profiles/emacs/emacs/bin/plantuml") (setq org-plantuml-exec-mode 'plantuml) (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))) #+end_src *** Restclient [[https://github.com/pashky/restclient.el][restclient.el]] is an Emacs package to send HTTP requests. [[https://github.com/alf/ob-restclient.el][ob-restclient]] provides interaction with Org Babel. References: - [[https://joseph8th.github.io/posts/wow-writing-literate-api-documentation-in-emacs-org-mode/][WOW! Writing Literate API Documentation in Emacs Org Mode]] #+begin_src emacs-lisp (use-package restclient :if (not my/remote-server) :straight t :config (general-define-key :keymaps 'restclient-mode-map :states '(normal visual) "RET" #'restclient-http-send-current "M-RET" #'restclient-http-send-current-stay-in-window "y" nil "M-y" #'restclient-copy-curl-command) (general-define-key :keymaps 'restclient-response-mode-map :states '(normal visual) "q" #'quit-window)) (use-package ob-restclient :after (org restclient) :if (not my/remote-server) :straight t) #+end_src *** Org Babel Setup Enable languages #+begin_src emacs-lisp (with-eval-after-load-norem 'org (org-babel-do-load-languages 'org-babel-load-languages `((emacs-lisp . t) (python . t) (sql . t) ;; (typescript .t) (hy . t) (shell . t) (plantuml . t) (octave . t) ,@(unless my/is-termux '((jupyter . t))) (sparql . t))) (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)) #+end_src Use Jupyter block instead of built-in Python. #+begin_src emacs-lisp (with-eval-after-load 'ob-jupyter (org-babel-jupyter-override-src-block "python") (org-babel-jupyter-override-src-block "hy")) #+end_src Turn of some minor modes in source blocks. #+begin_src emacs-lisp (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))) #+end_src Async code blocks evaluations. Jupyter blocks have a built-in async, so they are set as ignored. #+begin_src emacs-lisp (use-package ob-async :straight t :after (org) :config (setq ob-async-no-async-languages-alist '("python" "hy" "jupyter-python" "jupyter-octave" "restclient"))) #+end_src *** 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: #+begin_example #+PROPERTY: header-args:python :session #+end_example ~my/jupyter-connect-repl~ opens a =emacs-jupyter= REPL, connected to an active kernel. ~my/jupyter-qtconsole~ runs a standalone Jupyter QtConsole. Requirements: =ss= #+begin_src emacs-lisp (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)))) #+end_src 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. #+begin_src emacs-lisp (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)))))) #+end_src *** Output post-processing **** Do not wrap the output in emacs-jupyter Emacs-jupyter has its own insertion mechanisms, which always prepends output statements with =:=. That is not desirable in cases where a kernel supports only plain output, e.g. calysto_hy kernel. So there we have a minor mode that overrides this behavior. #+begin_src emacs-lisp (defun my/jupyter-org-scalar (value) (cond ((stringp value) value) (t (jupyter-org-scalar value)))) (define-minor-mode my/emacs-jupyter-raw-output "Make emacs-jupyter do raw output") (defun my/jupyter-org-scalar-around (fun value) (if my/emacs-jupyter-raw-output (my/jupyter-org-scalar value) (funcall fun value))) (with-eval-after-load 'jupyter (advice-add 'jupyter-org-scalar :around #'my/jupyter-org-scalar-around)) #+end_src **** Wrap source code output A function to remove the :RESULTS: drawer from results. Once again, it's necessary because emacs-jupyter doesn't seem to respect =:results raw=. #+begin_src emacs-lisp (defun my/org-strip-results (data) (replace-regexp-in-string ":\\(RESULTS\\|END\\):\n" "" data)) #+end_src And an all-in-one function to: - prepend =#+NAME:= and =#+CAPTION:= to the source block output. Useful if the output is an image. - strip the :RESULTS: drawer from the output, if necessary - wrap results in the =src= block As for now, it looks sufficient to format source code outputs to get a tolerable LaTeX. #+begin_src emacs-lisp (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))))) #+end_src To use, add the following snippet to the org file: #+begin_example #+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 #+end_example Example usage: #+begin_example :post out_wrap(name="fig:chart", caption="График", data=*this*) #+end_example **** Apply ANSI color codes *SOURCE*: [[https://emacs.stackexchange.com/questions/44664/apply-ansi-color-escape-sequences-for-org-babel-results][Apply ANSI color escape sequences for Org Babel results]] A minor mode to apply ANSI color codes after execution. #+begin_src emacs-lisp (defun my/babel-ansi () (when-let ((beg (org-babel-where-is-src-block-result nil nil))) (save-excursion (goto-char beg) (when (looking-at org-babel-result-regexp) (let ((end (org-babel-result-end)) (ansi-color-context-region nil)) (ansi-color-apply-on-region beg end)))))) (define-minor-mode org-babel-ansi-colors-mode "Apply ANSI color codes to Org Babel results." :global t :after-hook (if org-babel-ansi-colors-mode (add-hook 'org-babel-after-execute-hook #'my/babel-ansi) (remove-hook 'org-babel-after-execute-hook #'my/babel-ansi))) #+end_src *** Executing stuff A few convinient functions and keybindings to execute things in an org buffer. First, execute things above and below the point: #+begin_src emacs-lisp (defun my/org-babel-execute-buffer-below (&optional arg) (interactive "P") (org-babel-eval-wipe-error-buffer) (let ((point (point))) (org-save-outline-visibility t (org-babel-map-executables nil (when (>= (point) point) (if (memq (org-element-type (org-element-context)) '(babel-call inline-babel-call)) (org-babel-lob-execute-maybe) (org-babel-execute-src-block arg))))))) (defun my/org-babel-execute-buffer-above (&optional arg) (interactive "P") (org-babel-eval-wipe-error-buffer) (let ((point (point))) (org-save-outline-visibility t (org-babel-map-executables nil (when (<= (point) point) (if (memq (org-element-type (org-element-context)) '(babel-call inline-babel-call)) (org-babel-lob-execute-maybe) (org-babel-execute-src-block arg))))))) #+end_src Some keybindings: #+begin_src emacs-lisp (with-eval-after-load 'org (general-define-key :keymaps 'org-babel-map "B" #'my/org-babel-execute-buffer-below "A" #'my/org-babel-execute-buffer-above) (my-leader-def :keymaps 'org-mode-map "SPC b" '(:wk "org-babel") "SPC b" org-babel-map)) #+end_src *** Managing a literate programming project A few tricks to do literate programming. I actually have only one ([[https://github.com/SqrtMinusOne/sqrt-data][sqrt-data]]), and I'm not convinced in the benefits of the approach... Anyway, Org files are better off in a separated directory (e.g. =org=). So I've come up with the following solution to avoid manually prefixing the =:tangle= arguments. Set up the following argument with the path to the project root: #+begin_example #+PROPERTY: PRJ-DIR .. #+end_example A function to do the prefixing: #+begin_src emacs-lisp (defun my/org-prj-dir (path) (expand-file-name path (org-entry-get nil "PRJ-DIR" t))) #+end_src Example usage is as follows: #+begin_example :tangle (my/org-prj-dir "sqrt_data/api/__init__.py") #+end_example ** Tools Various small packages. *** Presentations Doing presentations with [[https://github.com/rlister/org-present][org-present]]. #+begin_src emacs-lisp (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 "" 'my/present-next-with-latex "" 'my/present-prev-with-latex) (setq org-present-mode-hook (list (lambda () (blink-cursor-mode 0) (org-present-big) (org-bars-mode -1) ;; (org-display-inline-images) (org-present-hide-cursor) (org-present-read-only) (display-line-numbers-mode 0) (hide-mode-line-mode +1) (setq-local org-format-latex-options (plist-put org-format-latex-options :scale (* org-present-text-scale my/org-latex-scale 0.5))) ;; (org-latex-preview '(16)) ;; TODO ^somehow this stucks at running LaTeX^ (setq-local olivetti-body-width 60) (olivetti-mode 1)))) (setq org-present-mode-quit-hook (list (lambda () (blink-cursor-mode 1) (org-present-small) (org-bars-mode 1) ;; (org-remove-inline-images) (org-present-show-cursor) (org-present-read-write) (display-line-numbers-mode 1) (hide-mode-line-mode 0) (setq-local org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale)) (org-latex-preview '(64)) (olivetti-mode -1) (setq-local olivetti-body-width (default-value 'olivetti-body-width)))))) #+end_src *** TOC Make a TOC inside the org file. References: - [[https://github.com/alphapapa/org-make-toc][alphapapa/org-make-toc]] #+begin_src emacs-lisp (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) #+end_src *** Screenshots A nice package to make screenshots and insert them to the Org document. #+begin_src emacs-lisp (use-package org-attach-screenshot :commands (org-attach-screenshot) :straight t) #+end_src *** Transclusion A package that implements transclusions in Org Mode, i.e. rendering part of one file inside another file. #+begin_src emacs-lisp (use-package org-transclusion :after org :straight (:host github :repo "nobiot/org-transclusion") :config (add-to-list 'org-transclusion-extensions 'org-transclusion-indent-mode) (require 'org-transclusion-indent-mode) (general-define-key :keymaps '(org-transclusion-map) :states '(normal) "RET" #'org-transclusion-open-source "gr" #'org-transclusion-refresh) (general-define-key :keymaps '(org-mode-map) :states 'normal "C-c t a" #'org-transclusion-add "C-c t A" #'org-transclusion-add-all "C-c t t" #'org-transclusion-mode)) #+end_src *** Drawing This package is unbelievably good. I would have never thought it's even possible to have this in Emacs. #+begin_src emacs-lisp (use-package edraw-org :straight (:host github :repo "misohena/el-easydraw") :if (and (not my/is-termux) (not my/remote-server)) :after (org) :config (edraw-org-setup-default)) #+end_src *** 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. #+begin_src emacs-lisp (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"))))) #+end_src ** Productivity & Knowledge management My ongoing effort to +get a productivity setup+ manage something in my life in Org. Initial inspirations (<2021-06-30 Wed>): - [[https://www.labri.fr/perso/nrougier/GTD/index.html][Nicolas P. Rougier. Get Things Done with Emacs]] - [[https://blog.jethro.dev/posts/org_mode_workflow_preview/][Jetro Kuan. Org-mode Workflow]] - [[https://www.alexeyshmalko.com/how-i-note/][Alexey Shmalko: How I note]] - [[https://rgoswami.me/posts/org-note-workflow/][Rohit Goswami: An Orgmode Note Workflow]] Current status of what I ended up using (<2023-12-16 Sat>): - [[*Org Journal][org-journal]] for keeping a journal - [[*Org Roam][org-roam]] for a knowledge base. - org-agenda with org-clock for tasks *** Org Agenda & Project Management This section had seen a lot of experimentation over the last... well, years. **** Agenda & refile files All my project files live in the =/projects= directory, so here's a function to set up =org-agenda-files= and =org-refile-targets= accordingly. Also, my project structure is somewhat chaotic, so I have an =.el= file in the org directory that defines some of the refile targets. #+begin_src emacs-lisp (defun my/update-org-agenda () (interactive) (let ((project-files (mapcar (lambda (f) (concat org-directory "/projects/" f)) (seq-filter (lambda (f) (not (file-directory-p f))) (directory-files (concat org-directory "/projects")))))) (setq org-agenda-files `("inbox.org" "misc/habit.org" "contacts.org" ,@project-files)) (setq org-refile-targets `(,@(mapcar (lambda (f) `(,f . (:tag . "refile"))) project-files) ,@(mapcar (lambda (f) `(,f . (:regexp . "Tasks"))) project-files))) (when (file-exists-p (concat org-directory "/scripts/refile.el")) (load-file (concat org-directory "/scripts/refile.el")) (run-hooks 'my/org-refile-hooks)))) (with-eval-after-load-norem 'org (setq org-roam-directory (concat org-directory "/roam")) (my/update-org-agenda)) #+end_src Refile settings #+begin_src emacs-lisp (setq org-refile-use-outline-path 'file) (setq org-outline-path-complete-in-steps nil) #+end_src My day ends late sometimes. Thanks John Wigley. #+begin_src emacs-lisp (setq org-extend-today-until 4) #+end_src **** Capture templates Settings for Org capture mode. The goal here is to have a non-disruptive process to capture various ideas. #+begin_src emacs-lisp (defun my/generate-inbox-note-name () (format "%s/inbox-notes/%s%s.org" org-directory (format-time-string "%Y%m%d%H%M%S") (let ((note-name (read-string "Note name: "))) (if (not (string-empty-p note-name)) (string-replace " " "-" (concat "-" (downcase note-name))) "")))) (setq org-capture-templates `(("i" "Inbox" entry (file "inbox.org") ,(concat "* TODO %?\n" "/Entered on/ %U")) ("e" "email" entry (file "inbox.org") ,(concat "* TODO %:from %:subject \n" "/Entered on/ %U\n" "/Received on/ %:date-timestamp-inactive\n" "%a\n")) ("f" "elfeed" entry (file "inbox.org") ,(concat "* TODO %:elfeed-entry-title\n" "/Entered on/ %U\n" "%a\n")) ("n" "note" plain (file my/generate-inbox-note-name) ,(concat "#+TODO: PROCESSED(p)\n" "\n" "* %?\n" "/Entered on/ %U")))) #+end_src **** org-clock & org-clock-agg [[https://orgmode.org/manual/Clocking-Work-Time.html][org-clock]] allows for tracking time spent in Org entries. [[https://github.com/SqrtMinusOne/org-clock-agg][org-clock-agg]] is my package for creating reports from org-clock records. It's been somewhat complicated to integrate into my workflow, but I think it's been worth it because I can now create reports for: - how much time i spent on which category of tasks (education / job / ...); - time spent per activity, particularly time spent on meetings per category; - time spent per project - ... #+begin_src emacs-lisp (use-package org-clock-agg :straight (:host github :repo "SqrtMinusOne/org-clock-agg") :commands (org-clock-agg) :init (with-eval-after-load 'org (my-leader-def "ol" #'org-clock-agg))) #+end_src The following enables org-clock persistence between Emacs sessions. #+begin_src emacs-lisp (with-eval-after-load 'org (setq org-clock-persist 'clock) (org-clock-persistence-insinuate)) #+end_src Effort estimation. Not using this as of now. #+begin_src emacs-lisp (with-eval-after-load-norem 'org (add-to-list 'org-global-properties '("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 1:30 2:00 4:00 8:00"))) #+end_src Log DONE time #+begin_src emacs-lisp (setq org-log-done 'time) #+end_src ***** Custom modeline positioning I wanted =org-mode-line-string= to be prepended to =global-mode-string= rather than appended, but somehow the modeline stops working if =org-mode-line-string= is the first element... So I'll at least put it before my =exwm-modeline-segment=. #+begin_src emacs-lisp (defun my/org-clock-in--fix-mode-line () (when (memq 'org-mode-line-string global-mode-string) (let (new-global-mode-string appended (is-first t)) (dolist (item global-mode-string) (cond ((or (equal item '(:eval (exwm-modeline-segment))) (equal item '(:eval (persp-mode-line)))) (unless appended (when is-first (push "" new-global-mode-string)) (push 'org-mode-line-string new-global-mode-string) (setq appended t)) (push item new-global-mode-string)) ((equal item 'org-mode-line-string)) (t (push item new-global-mode-string))) (setq is-first nil)) (unless appended (push 'org-mode-line-string new-global-mode-string)) (setq global-mode-string (nreverse new-global-mode-string))))) (add-hook 'org-clock-in-hook #'my/org-clock-in--fix-mode-line) #+end_src ***** Prompt start time for org-clock-in Support prompting for start time for =org-clock-in=: #+begin_src emacs-lisp (defun my/org-clock-in-prompt-time (&optional select) (interactive "P") (org-clock-in select (encode-time (org-parse-time-string (org-read-date t))))) (with-eval-after-load 'org (my-leader-def :keymaps 'org-mode-map :infix "SPC" "I" #'my/org-clock-in-prompt-time)) #+end_src ***** Put total clocked time in properties By default, =org-clock= stores its results only in the =:LOGBOOK:= drawer, which doesn't get parsed by =org-element-at-point=. As such, clock resutls are inaccessible from =org-ql=. This ensures that the total clocked time is also saved in the =:PROPERTIES:= drawer. We can get the clocked value in minutes with =org-clock-sum=. This weird function stores what I need in buffer-local variables and text-properties. #+begin_src emacs-lisp (defun my/org-clock-get-total-minutes-at-point () "Get total clocked time for heading at point." (let* ((element (org-element-at-point-no-context)) (s (buffer-substring-no-properties (org-element-property :begin element) (org-element-property :end element)))) (with-temp-buffer (insert s) (org-clock-sum) org-clock-file-total-minutes))) #+end_src And use the function to set the total clocked time. #+begin_src emacs-lisp (defconst my/org-clock-total-prop :CLOCK_TOTAL) (defun my/org-clock-set-total-clocked () "Set total clocked time for heading at point." (interactive) (save-excursion (org-back-to-heading t) (org-set-property (substring (symbol-name my/org-clock-total-prop) 1) (org-duration-from-minutes (my/org-clock-get-total-minutes-at-point))))) (add-hook 'org-clock-in-hook #'my/org-clock-set-total-clocked) (add-hook 'org-clock-out-hook #'my/org-clock-set-total-clocked) (add-hook 'org-clock-cancel-hook #'my/org-clock-set-total-clocked) #+end_src ***** Switch between recently clocked items #+begin_src emacs-lisp (defun my/org-clock-recent () (interactive) (let* ((entries (org-ql-query :select #'element-with-markers :from (org-agenda-files) :where '(clocked :from -1))) (entries-data (mapcar (lambda (e) (cons (org-element-property :raw-value e) e)) entries))) (unless entries (user-error "No recently clocked entries!")) entries-data (let* ((entry (alist-get (completing-read "Entry: " entries-data) entries-data nil nil #'equal)) (marker (org-element-property :org-marker entry))) (pop-to-buffer-same-window (marker-buffer marker)) (goto-char marker)))) (with-eval-after-load 'org (my-leader-def :keymaps 'org-mode-map :infix "SPC" "C" #'my/org-clock-recent)) #+end_src **** org-super-agenda [[https://github.com/alphapapa/org-super-agenda][org-super-agenda]] is alphapapa's extension to group items in org-agenda. I don't use it instead of the standard agenda, but =org-ql= uses it for some of its views. #+begin_src emacs-lisp (use-package org-super-agenda :straight t :after (org) :config ;; Alphapapa doesn't like evil (general-define-key :keymaps '(org-super-agenda-header-map) "h" nil "j" nil "k" nil "l" nil) (org-super-agenda--def-auto-group outline-path-file "their outline paths & files" :key-form (org-super-agenda--when-with-marker-buffer (org-super-agenda--get-marker item) ;; org-ql depends on f and s anyway (s-join "/" (cons (f-filename (buffer-file-name)) (org-get-outline-path)))))) #+end_src It doesn't look great with org-bars mode, so... #+begin_src emacs-lisp (defun my/org-super-agenda--make-agenda-header-around (fun name) (remove-text-properties 0 (length name) '(line-prefix nil) name) (remove-text-properties 0 (length name) '(wrap-prefix nil) name) (funcall fun (substring-no-properties name))) (with-eval-after-load 'org-super-agenda (advice-add 'org-super-agenda--make-agenda-header :around #'my/org-super-agenda--make-agenda-header-around)) #+end_src **** org-ql [[https://github.com/alphapapa/org-ql][org-ql]] is a package to query org files. #+begin_src emacs-lisp (use-package org-ql :after (org) :if (not my/remote-server) :straight t :init ;; See https://github.com/alphapapa/org-ql/pull/237 (setq org-ql-regexp-part-ts-time (rx " " (repeat 1 2 digit) ":" (repeat 2 digit) (optional "-" (repeat 1 2 digit) ":" (repeat 2 digit)))) (my-leader-def :infix "o" "v" #'org-ql-view "q" #'org-ql-search)) #+end_src ***** Add :multi argument to the property predicate I use the property predicate to find tasks linked to meetings, and I want to link some tasks to multiple meetings. So I modified the property predicate to support that. I can't contribute that back to =org-ql= because it requires copyright assignment, so here it is. #+begin_src emacs-lisp (with-eval-after-load 'org-ql (org-ql-defpred property (property &optional value &key inherit multi) "Return non-nil if current entry has PROPERTY, and optionally VALUE. If INHERIT is nil, only match entries with PROPERTY set on the entry; if t, also match entries with inheritance. If INHERIT is not specified, use the Boolean value of `org-use-property-inheritance', which see (i.e. it is only interpreted as nil or non-nil). If MULTI is non-nil, also check for multi-value properties." :normalizers ((`(,predicate-names) ;; HACK: This clause protects against the case in ;; which the arguments are nil, which would cause an ;; error in `rx-to-string' in other clauses. This ;; can happen with `org-ql-completing-read', ;; e.g. when the input is "property:" while the user ;; is typing. ;; FIXME: Instead of this being moot, make this ;; predicate test for whether an entry has local ;; properties when no arguments are given. (list 'property "")) (`(,predicate-names ,property ,value . ,plist) ;; Convert keyword property arguments to strings. Non-sexp ;; queries result in keyword property arguments (because to do ;; otherwise would require ugly special-casing in the parsing). (when (keywordp property) (setf property (substring (symbol-name property) 1))) (list 'property property value :inherit (if (plist-member plist :inherit) (plist-get plist :inherit) org-use-property-inheritance) :multi (when (plist-member plist :multi) (plist-get plist :multi))))) ;; MAYBE: Should case folding be disabled for properties? What about values? ;; MAYBE: Support (property) without args. ;; NOTE: When inheritance is enabled, the preamble can't be used, ;; which will make the search slower. :preambles ((`(,predicate-names ,property ,value ,(map :multi) . ,(map :inherit)) ;; We do NOT return nil, because the predicate still needs to be tested, ;; because the regexp could match a string not inside a property drawer. (list :regexp (unless inherit (rx-to-string `(seq bol (0+ space) ":" ,property ,@(when multi '((? "+"))) ":" (1+ space) ,value (0+ space) eol))) :query query)) (`(,predicate-names ,property ,(map :multi) . ,(map :inherit)) ;; We do NOT return nil, because the predicate still needs to be tested, ;; because the regexp could match a string not inside a property drawer. ;; NOTE: The preamble only matches if there appears to be a value. ;; A line like ":ID: " without any other text does not match. (list :regexp (unless inherit (rx-to-string `(seq bol (0+ space) ":" ,property ,@(when multi '((? "+"))) ":" (1+ space) (minimal-match (1+ not-newline)) eol))) :query query))) :body (pcase property ('nil (user-error "Property matcher requires a PROPERTY argument")) (_ (pcase value ('nil ;; Check that PROPERTY exists (org-ql--value-at (point) (lambda () (org-entry-get (point) property)))) (_ ;; Check that PROPERTY has VALUE. ;; TODO: Since --value-at doesn't account for inheritance, ;; we should generalize --tags-at to also work for property ;; inheritance and use it here, which should be much faster. (if multi (when-let (values (org-ql--value-at (point) (lambda () ;; The default separator is space (let ((org-property-separators `((,property . "\n")))) (org-entry-get (point) property inherit))))) (seq-some (lambda (v) (string-equal value v)) (split-string values "\n"))) (string-equal value (org-ql--value-at (point) (lambda () (org-entry-get (point) property inherit))))))))))) #+end_src ***** Recent items I just want to change the default grouping in =org-ql-view-recent-items=... #+begin_src emacs-lisp (cl-defun my/org-ql-view-recent-items (&key num-days (type 'ts) (files (org-agenda-files)) (groups '((:auto-outline-path-file t) (:auto-todo t)))) "Show items in FILES from last NUM-DAYS days with timestamps of TYPE. TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or `closed'." (interactive (list :num-days (read-number "Days: ") :type (->> '(ts ts-active ts-inactive clocked closed) (completing-read "Timestamp type: ") intern))) ;; It doesn't make much sense to use other date-based selectors to ;; look into the past, so to prevent confusion, we won't allow them. (-let* ((query (pcase-exhaustive type ((or 'ts 'ts-active 'ts-inactive) `(,type :from ,(- num-days) :to 0)) ((or 'clocked 'closed) `(,type :from ,(- num-days) :to 0))))) (org-ql-search files query :title "Recent items" :sort '(todo priority date) :super-groups groups))) #+end_src ***** Return all TODOs A view to return all TODOs in a category. #+begin_src emacs-lisp (defun my/org-ql-all-todo () (interactive) ;; The hack I borrowed from notmuch to make " " a separator (let* ((crm-separator " ") (crm-local-completion-map (let ((map (make-sparse-keymap))) (set-keymap-parent map crm-local-completion-map) (define-key map " " 'self-insert-command) map)) (ivy-prescient-sort-commands nil) (categories (completing-read-multiple "Categories: " '("TEACH" "EDU" "JOB" "LIFE" "CONFIG")))) (org-ql-search (org-agenda-files) `(and (todo) ,@(unless (seq-empty-p categories) `((category ,@categories)))) :sort '(priority todo deadline) :super-groups '((:auto-outline-path-file t))))) #+end_src ***** Configuring views Putting all the above in =org-ql-views=. #+begin_src emacs-lisp (setq org-ql-views (list (cons "Overview: All TODO" #'my/org-ql-all-todo) (cons "Review: Stale tasks" (list :buffers-files #'org-agenda-files :query '(and (todo) (not (ts :from -14))) :title "Review: Stale tasks" :sort '(todo priority date) :super-groups '((:auto-outline-path-file t)))) (cons "Review: Recently timestamped" #'my/org-ql-view-recent-items) (cons "Review: Unlinked to meetings" (list :buffers-files #'org-agenda-files :query '(and (todo "DONE" "NO") (not (property "MEETING")) (ts :from -7)) :super-groups '((:auto-outline-path-file t)))) (cons "Review: Meeting" #'my/org-ql-meeting-tasks))) #+end_src ***** Custom format element Changing the default =org-ql-view--format-element= to include effort estimation and the clocked time. I wish it were more configurable out-of-the-box. #+begin_src emacs-lisp (defun my/org-ql-view--format-element-override (element) "Format ELEMENT for `org-ql-view'. Check `org-ql-view--format-element' for the original implementation and lots of comments which are too long for my Emacs config." (if (not element) "" (setf element (org-ql-view--resolve-element-properties element)) (let* ((properties (cadr element)) (properties (cl-loop for (key val) on properties by #'cddr for symbol = (intern (cl-subseq (symbol-name key) 1)) unless (member symbol '(parent)) append (list symbol val))) (title (--> (org-ql-view--add-faces element) (org-element-property :raw-value it) (org-link-display-format it))) (todo-keyword (-some--> (org-element-property :todo-keyword element) (org-ql-view--add-todo-face it))) (tag-list (if org-use-tag-inheritance (if-let ((marker (or (org-element-property :org-hd-marker element) (org-element-property :org-marker element)))) (with-current-buffer (marker-buffer marker) (org-with-wide-buffer (goto-char marker) (cl-loop for type in (org-ql--tags-at marker) unless (or (eq 'org-ql-nil type) (not type)) append type))) (display-warning 'org-ql (format "No marker found for item: %s" title)) (org-element-property :tags element)) (org-element-property :tags element))) (tag-string (when tag-list (--> tag-list (s-join ":" it) (s-wrap it ":") (org-add-props it nil 'face 'org-tag)))) ;; (category (org-element-property :category element)) (priority-string (-some->> (org-element-property :priority element) (char-to-string) (format "[#%s]") (org-ql-view--add-priority-face))) (clock-string (let ((effort (org-element-property :EFFORT element)) (clocked (org-element-property my/org-clock-total-prop element))) (cond ((and clocked effort) (format "[%s/%s]" clocked effort)) ((and clocked (not effort) (format "[%s]" clocked))) ((and (not clocked) effort) (format "[EST: %s]" effort))))) (habit-property (org-with-point-at (or (org-element-property :org-hd-marker element) (org-element-property :org-marker element)) (when (org-is-habit-p) (org-habit-parse-todo)))) (due-string (pcase (org-element-property :relative-due-date element) ('nil "") (string (format " %s " (org-add-props string nil 'face 'org-ql-view-due-date))))) (string (s-join " " (-non-nil (list todo-keyword priority-string title due-string clock-string tag-string))))) (remove-list-of-text-properties 0 (length string) '(line-prefix) string) (--> string (concat " " it) (org-add-props it properties 'org-agenda-type 'search 'todo-state todo-keyword 'tags tag-list 'org-habit-p habit-property))))) (with-eval-after-load 'org-ql (advice-add #'org-ql-view--format-element :override #'my/org-ql-view--format-element-override)) #+end_src **** Link tasks to meetings The workflow here is basically link some tasks to meeting(s) and create a report from tasks linked to a particular meetings. The report also shows the time spent per task thanks to the last modification to =org-ql=. This is essentially to avoid having to scramble my mind for hints of what I'm supposed to tell I was doing at each meeting. #+begin_src emacs-lisp (defun my/org-meeting--prompt () (let* ((meetings (org-ql-query :select #'element-with-markers :from (org-agenda-files) :where '(and (todo) (tags "mt") (ts-active :from today to 31)) :order-by 'scheduled)) (data (mapcar (lambda (meeting) (let ((raw-value (org-element-property :raw-value meeting)) (scheduled (org-format-timestamp (org-element-property :scheduled meeting) (cdr org-time-stamp-formats)))) (cons (format "%-30s %s" raw-value (propertize scheduled 'face 'org-agenda-date)) meeting))) meetings)) (ivy-prescient-sort-commands nil)) (cdr (assoc (completing-read "Meeting: " data nil t) data)))) (defun my/org-meeting--format-link (meeting) (format "[[file:%s::*%s][%s]]" (buffer-file-name (marker-buffer (org-element-property :org-marker meeting))) (org-element-property :raw-value meeting) (org-element-property :raw-value meeting))) (defun my/org-meeting-link (&optional arg) (interactive "p") (save-excursion (org-back-to-heading t) (let* ((meeting (my/org-meeting--prompt)) (link (my/org-meeting--format-link meeting)) (element (org-element-at-point-no-context))) (if (or (not arg) (not (org-element-property :MEETING element))) (org-set-property "MEETING" link) (let ((range (org-get-property-block (org-element-property :begin element))) (case-fold-search nil)) (goto-char (cdr range)) (beginning-of-line) (insert-and-inherit ":MEETING+: " link "\n") (org-indent-line)))))) (defun my/org-ql-meeting-tasks (meeting) (interactive (list (my/org-meeting--prompt))) (org-ql-search (org-agenda-files) `(property "MEETING" ,(my/org-meeting--format-link meeting) :multi t) :sort '(date priority todo) :buffer (format "*Meeting Tasks: %s*" (org-element-property :raw-value meeting)) :super-groups '((:auto-outline-path t)))) (defun my/org-ql-meeting-tasks-agenda () (interactive) (let ((meeting (save-window-excursion (org-agenda-switch-to) (org-back-to-heading) (org-ql--add-markers (org-element-at-point))))) (my/org-ql-meeting-tasks meeting))) (with-eval-after-load 'org-agenda (general-define-key :keymaps 'org-agenda-mode-map :states '(normal motion) "gm" #'my/org-ql-meeting-tasks-agenda)) #+end_src **** Tracking habits Let's see how this goes. References: - https://orgmode.org/manual/Tracking-your-habits.html [[https://github.com/ml729/org-habit-stats][org-habit-stats]] is a pretty nice package. Using my fork until my PR is merged. #+begin_src emacs-lisp (use-package org-habit-stats :straight (:host github :repo "ml729/org-habit-stats") :after (org) :config (general-define-key :keymaps '(org-habit-stats-mode-map) :states '(normal emacs) "q" #'org-habit-stats-exit "<" #'org-habit-stats-calendar-scroll-left ">" #'org-habit-stats-calendar-scroll-right "[" #'org-habit-stats-scroll-graph-left "]" #'org-habit-stats-scroll-graph-right "{" #'org-habit-stats-scroll-graph-left-big "}" #'org-habit-stats-scroll-graph-right-big "." #'org-habit-stats-view-next-habit "," #'org-habit-stats-view-previous-habit) (add-hook 'org-after-todo-state-change-hook 'org-habit-stats-update-properties)) #+end_src **** Custom agendas Some custom agendas to fit my workflow. See [[https://emacs.stackexchange.com/questions/18179/org-agenda-command-with-org-agenda-filter-by-tag-not-working][this answer]] at Emacs StackExchange for filtering the =agenda= block by tag: #+begin_src emacs-lisp (defun my/org-match-at-point-p (match) "Return non-nil if headline at point matches MATCH. Here MATCH is a match string of the same format used by `org-tags-view'." (funcall (cdr (org-make-tags-matcher match)) (org-get-todo-state) (org-get-tags-at) (org-reduced-level (org-current-level)))) (defun my/org-agenda-skip-without-match (match) "Skip current headline unless it matches MATCH. Return nil if headline containing point matches MATCH (which should be a match string of the same format used by `org-tags-view'). If headline does not match, return the position of the next headline in current buffer. Intended for use with `org-agenda-skip-function', where this will skip exactly those headlines that do not match." (save-excursion (unless (org-at-heading-p) (org-back-to-heading)) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (if (my/org-match-at-point-p match) nil next-headline)))) #+end_src And the agendas themselves: #+begin_src emacs-lisp (defun my/org-scheduled-get-time () (let ((scheduled (org-get-scheduled-time (point)))) (if scheduled (format-time-string "%Y-%m-%d" scheduled) ""))) (setq org-agenda-hide-tags-regexp (rx (or "org" "refile" "proj" "habit"))) (setq org-agenda-custom-commands `(("p" "My outline" ((agenda "" ((org-agenda-skip-function '(my/org-agenda-skip-without-match "-habit")))) (tags-todo "inbox" ((org-agenda-overriding-header "Inbox") (org-agenda-prefix-format " %i %-12:c") (org-agenda-hide-tags-regexp "."))) (tags-todo "+waitlist+SCHEDULED<=\"<+14d>\"" ((org-agenda-overriding-header "Waitlist") (org-agenda-hide-tags-regexp "waitlist") (org-agenda-prefix-format " %i %-12:c %-12(my/org-scheduled-get-time)"))) (tags-todo "habit+SCHEDULED<=\"<+0d>\"" ((org-agenda-overriding-header "Habits") (org-agenda-prefix-format " %i %-12:c") (org-agenda-hide-tags-regexp "."))))))) #+end_src **** Alerts - Me at 10:00: /Open Org Agenga/ oh, there's a meeting at 15:00 - Me at 14:00: /Open Org Agenda/ oh, there's a meeting at 15:00 - Me at 14:45: Gotta remember to join in 15 minutes - Me at 14:55: Gotta remember to join in 5 minutes - Me at 15:05: Sh*t Okay, I will set up +org-alert+ some custom alert system. I want to have multiple warnings, let it be 10 minutes in advance and 1 minute in advance for now. #+begin_src emacs-lisp (setq my/org-alert-notify-times '(600 60)) #+end_src And IDK if that makes much sense, but I'll try to avoid re-creating timers. So, here are functions to schedule showing some label at some time and to check whether the label is scheduled: #+begin_src emacs-lisp (setq my/org-alert--alerts (make-hash-table :test #'equal)) (defun my/org-alert--is-scheduled (label time) "Check if LABEL is scheduled to be shown an TIME." (gethash (cons label time) my/org-alert--alerts nil)) (defun my/org-alert--schedule (label time) "Schedule LABEL to be shown at TIME, unless it's already scheduled." (unless (my/org-alert--is-scheduled label time) (puthash (cons label time) (run-at-time time nil (lambda () (alert label :title "PROXIMITY ALERT"))) my/org-alert--alerts))) #+end_src And unschedule items that need to be unscheduled: #+begin_src emacs-lisp (defun my/org-alert-cleanup (&optional keys) "Unschedule items that do not appear in KEYS. KEYS is a list of cons cells like (