#+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 | |--------------------------+------------------------------------------| | ytel | 327340a95c4ff9cffd171f6bd937c6041f63add7 | | org-roam dailies | d2648918fcc338bd5c1cd6d5c0aa60a65077ccf7 | | org-roam projects | 025278a1e180e86f3aade20242e4ac1cdc1a2f13 | | treemacs | 3d87852745caacc0863c747f1fa9871d367240d2 | | tab-bar.el | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | spaceline | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | code compass | 8594d6f53e42c70bbf903e168607841854818a38 | | vue-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | svelte-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | pomidor | 8594d6f53e42c70bbf903e168607841854818a38 | | elfeed-score | 8e591e0d2afd909ae5be00caf17f9b17c6cd8b61 | | org-trello | 3f5967a5f63928ea9c8567d8d9f31e84cdbbc21f | | jabber | 9b0e73a4703ff35a2d30fd704200052888191217 | | wallabag | 9b0e73a4703ff35a2d30fd704200052888191217 | | conda | 609fc84e439b11ea5064f3a948079daebb654aca | | notmuch tags keybindings | eac134c5456051171c1c777254f503cc71ce12cd | | expand-region | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | org-latex-impatient | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | dired-single | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | progidy | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 | | tree-sitter | 1920a48aec49837d63fa88ca315928dc4e9d14c2 | | org-roam-protocol | 2f0c20eb01b8899d00d129cc7ca5c6b263c69c65 | | eshell-info-banner | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 | | aweshell | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 | | link tasks to meetings | 23496bfacc31ffedf2092da04e4e602b71373425 | | forge and code-review | bb1365d9a026cefb7051992a68176ea00f041abd | | phscroll | bb1365d9a026cefb7051992a68176ea00f041abd | | org-make-toc | bb1365d9a026cefb7051992a68176ea00f041abd | | edraw-org | bb1365d9a026cefb7051992a68176ea00f041abd | | sx.el | bb1365d9a026cefb7051992a68176ea00f041abd | | atomic-chrome | bb1365d9a026cefb7051992a68176ea00f041abd | | screenshot.el | bb1365d9a026cefb7051992a68176ea00f041abd | * 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 ** Modular configuration & environments I've made the config sort of modular because I need different parts in different environments. With such a large config as mine, this is important. The problem with one large =init.el= is it's evaluated fully in every envronment, which means =use-package= will try to download, compile, etc. every package, which takes a lot of time on termux. So this isn't about separation of concerns or something like that. Modules aren't independent; they're loaded in the same order as they are specified in this file; further modules may depend on previously loaded modules. Also, the modules are basically as large or small as they need to be required on each system. Obviously, there are a lot of compromises in this approach, but it works for me. For a more careful approach to modules, take a look, e.g., at [[https://protesilaos.com/emacs/dotemacs][Protesilaos']] config. First, I need to get the current environment: #+begin_src emacs-lisp (defun my/get-env () (or (getenv "EMACS_ENV") (when (member (system-name) '("dev-digital" "viridian")) "remote") (when (string-match-p (rx (* nonl) "com.termux" (* nonl)) (getenv "HOME")) "termux") "normal")) (setq my/env (my/get-env)) #+end_src Also, I sometimes need to know if a program is running inside Emacs (say, inside a terminal emulator). And sometimes I need to know if I'm running a nested Emacs session. To do that, I set the following environment variable: #+begin_src emacs-lisp (setq my/nested-emacs (and (getenv "IS_EMACS") t)) (setenv "IS_EMACS" "true") #+end_src Custom system name logic because on termux it's always "localhost". #+begin_src emacs-lisp (defun my/system-name () (or (getenv "ANDROID_NAME") (system-name))) #+end_src And a few legacy variables, which is true on termux. #+begin_src emacs-lisp (setq my/is-termux (equal (my/get-env) "termux")) (setq my/remote-server (equal (my/get-env) "remote")) #+end_src Below is the configuration table that shows which module is activated in which environment. #+NAME: modules-settings | Module name | normal | termux | remote | |-------------------+--------+--------+--------| | performance | + | + | + | | micromamba | + | | | | misc-initial | + | + | + | | keybindings | + | + | + | | termux | | + | | | i3 | + | | | | general-config | + | + | + | | wakatime | + | + | | | activitywatch | + | | | | general-ui | + | + | + | | doom-modeline | + | + | + | | perspective | + | + | + | | treemacs | + | + | | | lsp | + | | | | flycheck | + | + | + | | tree-sitter | + | + | + | | dap | | | | | reformatter | + | | | | copilot | | | | | web | + | | | | latex | + | + | | | markup | + | + | + | | lisp | + | + | + | | misc-programming | + | | | | general-org | + | + | | | org-literate | + | | | | org-productivity | + | + | | | org-export | + | | | | dired | + | + | + | | tramp | + | | | | terms | + | + | + | | dotfiles | + | + | + | | elfeed | + | | | | mail | + | | | | gnus | + | | | | emms | + | | | | misc-internet | + | + | + | | erc | + | | | | mastodon | + | | | | wallabag | | | | | ement | + | | | | telega | + | + | | | docs | + | + | | | ai | + | | | | index | + | + | | | pass | + | + | | | docker | + | | | | misc-apps | + | + | | | guix | + | | | | emacs-pinentry | | + | | | productivity-apps | + | + | | | chess | + | | | | discord | | | | | gource | + | | | I want this table to be updated automatically based on this file. So, first, specify how the modules are stored: #+begin_src emacs-lisp (setq my/modules-dir (file-name-as-directory (expand-file-name "modules" user-emacs-directory))) (setq my/modules-prefix "sqrt-") #+end_src Then, scan =Emacs.org= for headings with the =MODULE_NAME= property and set the tangle property: #+begin_src emacs-lisp (defun my/modules--refresh-and-list () (let (modules-list headlines) (org-element-map (org-element-parse-buffer) 'headline (lambda (elem) (push elem headlines))) (mapc (lambda (elem) (when-let ((module-name (org-element-property :MODULE_NAME elem))) (save-excursion (goto-char (org-element-property :begin elem)) (org-set-property "header-args:emacs-lisp" (concat ":tangle " my/modules-dir my/modules-prefix module-name ".el" " :comments links")) (push module-name modules-list)))) headlines) (seq-uniq modules-list))) #+end_src Then, read and update the settings table: #+begin_src emacs-lisp (defun my/modules--read-table () (save-excursion (goto-char (point-min)) (re-search-forward (rx bol "#+NAME: modules-settings")) (forward-line) (let* ((table (org-element-at-point)) (table-contents (mapcar (lambda (elem) (if (listp elem) (mapcar #'substring-no-properties elem) elem)) (org-table-to-lisp))) (envs-list (cdar table-contents))) (mapcar (lambda (elem) (cons (car elem) (cl-loop for sign in (cdr elem) for env in envs-list collect (cons env (not (string-empty-p sign)))))) (cddr table-contents))))) (defun my/modules--refresh-table (modules-list) (save-excursion (goto-char (point-min)) (search-forward "#+NAME: modules-settings") (let* ((table (org-element-at-point)) (table-contents (my/modules--read-table)) (envs-list (mapcar #'car (cdar table-contents)))) (delete-region (org-element-property :contents-begin table) (org-element-property :contents-end table)) (insert "\n" (string-trim (with-temp-buffer (insert "| Module name |" (mapconcat #'identity envs-list " | ") "\n") (insert "|--|\n") (dolist (module modules-list) (insert "| " module " | " (cl-loop for env in envs-list if (alist-get env (alist-get module table-contents nil nil #'equal) nil nil #'equal) concat " + |" else concat " |") "\n")) (goto-char (point-min)) (org-table-align) (buffer-substring-no-properties (point-min) (point-max)))))))) #+end_src Then, update the loading code that requires the necessary modules based on the environment: #+begin_src emacs-lisp (defun my/modules--refresh-loading () (let* ((modules-table (my/modules--read-table)) (envs-list (mapcar #'car (cdar modules-table)))) (save-excursion (goto-char (point-min)) (re-search-forward (rx bol "#+NAME: modules-loading")) (beginning-of-line 3) (delete-region (point) (save-excursion (re-search-forward (rx bol "#+end_src")) (end-of-line 0) (point))) (insert (pp-to-string `(pcase my/env ,@(mapcar (lambda (env) `(,env ,@(seq-filter #'identity (mapcar (lambda (elem) (when (alist-get env (cdr elem) nil nil #'equal) `(require ',(intern (concat my/modules-prefix (car elem)))))) modules-table)))) envs-list))))))) #+end_src Also delete unnecessary modules: #+begin_src emacs-lisp (defun my/modules--cleanup-folder (modules-list) (let ((source-files (seq-filter (lambda (f) (equal (file-name-extension f) "el")) (directory-files my/modules-dir))) (target-files (mapcar (lambda (m) (concat my/modules-prefix m ".el")) modules-list))) (dolist (extra-file (seq-difference source-files target-files)) (delete-file (concat my/modules-dir extra-file))))) #+end_src And a function that combines all of the above: #+begin_src emacs-lisp (defun my/modules-settings-refresh () (interactive) (org-fold-show-all) (let ((modules-list (my/modules--refresh-and-list))) (my/modules--refresh-table modules-list) (my/modules--refresh-loading) (my/modules--cleanup-folder modules-list) (setq my/modules-list modules-list))) #+end_src Now, add the modules directory to the =load-path= variable: #+begin_src emacs-lisp (push my/modules-dir load-path) #+end_src And require the necessary modules with the autogenerated code: #+NAME: modules-loading #+begin_src emacs-lisp (pcase my/env ("normal" (require 'sqrt-performance) (require 'sqrt-micromamba) (require 'sqrt-misc-initial) (require 'sqrt-keybindings) (require 'sqrt-i3) (require 'sqrt-general-config) (require 'sqrt-wakatime) (require 'sqrt-activitywatch) (require 'sqrt-general-ui) (require 'sqrt-doom-modeline) (require 'sqrt-perspective) (require 'sqrt-treemacs) (require 'sqrt-lsp) (require 'sqrt-flycheck) (require 'sqrt-tree-sitter) (require 'sqrt-reformatter) (require 'sqrt-web) (require 'sqrt-latex) (require 'sqrt-markup) (require 'sqrt-lisp) (require 'sqrt-misc-programming) (require 'sqrt-general-org) (require 'sqrt-org-literate) (require 'sqrt-org-productivity) (require 'sqrt-org-export) (require 'sqrt-dired) (require 'sqrt-tramp) (require 'sqrt-terms) (require 'sqrt-dotfiles) (require 'sqrt-elfeed) (require 'sqrt-mail) (require 'sqrt-gnus) (require 'sqrt-emms) (require 'sqrt-misc-internet) (require 'sqrt-erc) (require 'sqrt-mastodon) (require 'sqrt-ement) (require 'sqrt-telega) (require 'sqrt-docs) (require 'sqrt-ai) (require 'sqrt-index) (require 'sqrt-pass) (require 'sqrt-docker) (require 'sqrt-misc-apps) (require 'sqrt-guix) (require 'sqrt-productivity-apps) (require 'sqrt-chess) (require 'sqrt-gource)) ("termux" (require 'sqrt-performance) (require 'sqrt-misc-initial) (require 'sqrt-keybindings) (require 'sqrt-termux) (require 'sqrt-general-config) (require 'sqrt-wakatime) (require 'sqrt-general-ui) (require 'sqrt-doom-modeline) (require 'sqrt-perspective) (require 'sqrt-treemacs) (require 'sqrt-flycheck) (require 'sqrt-tree-sitter) (require 'sqrt-latex) (require 'sqrt-markup) (require 'sqrt-lisp) (require 'sqrt-general-org) (require 'sqrt-org-productivity) (require 'sqrt-dired) (require 'sqrt-terms) (require 'sqrt-dotfiles) (require 'sqrt-misc-internet) (require 'sqrt-telega) (require 'sqrt-docs) (require 'sqrt-index) (require 'sqrt-pass) (require 'sqrt-misc-apps) (require 'sqrt-emacs-pinentry) (require 'sqrt-productivity-apps)) ("remote" (require 'sqrt-performance) (require 'sqrt-misc-initial) (require 'sqrt-keybindings) (require 'sqrt-general-config) (require 'sqrt-general-ui) (require 'sqrt-doom-modeline) (require 'sqrt-perspective) (require 'sqrt-flycheck) (require 'sqrt-tree-sitter) (require 'sqrt-markup) (require 'sqrt-lisp) (require 'sqrt-dired) (require 'sqrt-terms) (require 'sqrt-dotfiles) (require 'sqrt-misc-internet))) #+end_src Finally, some post-processing of the tangled files: #+begin_src emacs-lisp (defun my/modules--post-tangle () (when (string-match-p (rx bos (literal my/modules-dir) (* nonl) ".el") (buffer-file-name)) (goto-char (point-min)) (insert ";;; -*- lexical-binding: t -*-\n") (goto-char (point-max)) (insert "\n(provide '" (file-name-base (buffer-file-name)) ")") (save-buffer) (message "Processed %s as emacs config module" (buffer-file-name)))) (add-hook 'org-babel-post-tangle-hook #'my/modules--post-tangle) #+end_src ** Performance :PROPERTIES: :MODULE_NAME: performance :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-performance.el :comments links :END: *** 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 package loading order and time. #+begin_src emacs-lisp (setq use-package-verbose nil) #+end_src #+begin_src emacs-lisp (setq use-package-compute-statistics 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.+ 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 :PROPERTIES: :MODULE_NAME: micromamba :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-micromamba.el :comments links :END: [[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 (ignore-errors (micromamba-activate "general"))) #+end_src ** Config files :PROPERTIES: :MODULE_NAME: misc-initial :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-initial.el :comments links :END: *** 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 :PROPERTIES: :MODULE_NAME: misc-initial :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-initial.el :comments links :END: *** 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 ** Scratch buffer :PROPERTIES: :MODULE_NAME: misc-initial :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-initial.el :comments links :END: I have a problem with =emacs-lisp-mode= as =initial-major-mode= because in my config it loads =lispy=, which loads =org-mode=. So until I've made a better loading screen, this will do. #+begin_src emacs-lisp (setq initial-major-mode 'fundamental-mode) (setq initial-scratch-message "Hello there <3\n\n") #+end_src * General settings ** Keybindings :PROPERTIES: :MODULE_NAME: keybindings :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-keybindings.el :comments links :END: *** 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)))) #+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 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 The following is necessary since my scratch buffer isn't lisp-interaction. #+begin_src emacs-lisp (defun my/lisp-interaction-buffer () (interactive) (let ((buf (get-buffer-create "*lisp-interaction*"))) (with-current-buffer buf (lisp-interaction-mode)) (switch-to-buffer buf))) #+end_src #+begin_src emacs-lisp (my-leader-def :infix "b" "" '(:which-key "buffers") "s" '(my/lisp-interaction-buffer :which-key "*lisp-interaction*") "m" '((lambda () (interactive) (persp-switch-to-buffer "*Messages*")) :which-key "*Messages*") "l" 'next-buffer "h" 'previous-buffer "k" 'kill-buffer ;; "b" 'persp-ivy-switch-buffer "b" #'persp-switch-to-buffer* "r" 'revert-buffer "u" 'ibuffer) #+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 outline-mode-map) :states '(normal motion) "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 ** Termux settings :PROPERTIES: :MODULE_NAME: termux :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-termux.el :comments links :END: For some reason my ONYX device has the tilde and escape wrong. #+begin_src emacs-lisp (when (and my/is-termux (not (equal (my/system-name) "snow"))) (define-key key-translation-map (kbd "`") (kbd "")) (define-key key-translation-map (kbd "") (kbd "`"))) #+end_src And the screen is less wide. #+begin_src emacs-lisp (when my/is-termux (setq split-width-threshold 90)) #+end_src ** i3 integration :PROPERTIES: :MODULE_NAME: i3 :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-i3.el :comments links :END: 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 (or my/remote-server my/nested-emacs) (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 :PROPERTIES: :MODULE_NAME: general-config :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-config.el :comments links :END: 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 (or (not (executable-find "xkb-switch")) (equal (string-trim (shell-command-to-string "xkb-switch -p")) "us")) (toggle-input-method) (my/run-in-background "xkb-switch -s us")))) #+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 The function below 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 **** 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 (defvar my/default-accents '((a . ä) (o . ö) (u . ü) (s . ß) (A . Ä) (O . Ö) (U . Ü) (S . ẞ))) (defun my/accent (arg) (interactive "P") (require 'accent) (message "%s" arg) (let* ((after? (eq accent-position 'after)) (char (if after? (char-after) (char-before))) (curr (intern (string char))) (default-diac (cdr (assoc curr my/default-accents)))) (if (and default-diac (not arg)) (progn (delete-char (if after? 1 -1)) (insert (format "%c" default-diac))) (call-interactively #'accent-company)))) (use-package accent :straight (:host github :repo "eliascotto/accent") :init (general-define-key :states '(normal) "gs" #'accent-company) (general-define-key :states '(normal insert) "M-n" #'my/accent) :commands (accent-menu) :config (general-define-key :keymaps 'popup-menu-keymap "C-j" #'popup-next "C-k" #'popup-previous "M-j" #'popup-next "M-k" #'popup-previous) (setq accent-custom '((a (ā)) (A (Ā))))) #+end_src *** Random editing tricks **** Round number at point #+begin_src emacs-lisp (defun my/round-number-at-point (word signs) (interactive (list (or (when (region-active-p) (buffer-substring-no-properties (region-beginning) (region-end))) (thing-at-point 'number 'no-properties)) (read-number "Decimal signs: " 2))) (when (stringp word) (setq word (string-to-number word))) (let ((number (/ (float (round (* (expt 10 signs) word))) (expt 10 signs)))) (save-excursion (replace-string-in-region (number-to-string word) (number-to-string number) (line-beginning-position) (line-end-position))))) #+end_src ** Working with projects :PROPERTIES: :MODULE_NAME: general-config :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-config.el :comments links :END: 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. | Type | Note | |------+-----------------------------------------------------------------------| | TODO | Maybe set =projectile-project-search-path= based on the folder index? | #+begin_src emacs-lisp (use-package projectile :straight t :config (projectile-mode +1) (general-define-key :keymaps 'projectile-command-map "b" #'consult-project-buffer)) (my-leader-def "p" '(:keymap projectile-command-map :which-key "projectile")) (general-nmap "C-p" #'projectile-find-file) #+end_src *** Git & Magit [[https://magit.vc/][Magit]] is a git interface for Emacs. A few CLI alternatives: - [[https://github.com/jesseduffield/lazygit][lazygit]]. I used it before Emacs - [[https://github.com/altsem/gitu][gitu]]. A CLI magit clone #+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 shows git changes for each line (added/changed/deleted lines). #+begin_src emacs-lisp (use-package git-gutter :straight t :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 | Guix dependency | |-----------------| | difftastic-bin | [[https://github.com/pkryger/difftastic.el][difftastic.el]] is a wrapper package for [[https://difftastic.wilfred.me.uk/][difftastic]]. #+begin_src emacs-lisp (use-package difftastic :straight t :commands (difftastic-magit-diff difftastic-magit-show difftastic-files difftastic-buffers) :init (with-eval-after-load 'magit-diff (transient-append-suffix 'magit-diff '(-1 -1) [("D" "Difftastic diff (dwim)" difftastic-magit-diff) ("S" "Difftastic show" difftastic-magit-show)]) (general-define-key :keymaps 'magit-blame-read-only-mode-map :states 'normal "D" #'difftastic-magit-show "S" #'difftastic-magit-show)) :config (setq difftastic-executable (executable-find "difft")) (general-define-key :keymaps 'difftastic-mode-map :states '(normal) "gr" #'difftastic-rerun "q" #'kill-buffer-and-window)) #+end_src My screen isn't wide enough to run =difftastic= in vertical split, so... #+begin_src emacs-lisp (defun my/difftastic-pop-at-bottom (buffer-or-name _requested-width) (let ((window (split-window-below))) (select-window window) (evil-move-window 'below)) (set-window-buffer (selected-window) buffer-or-name)) (setq difftastic-display-buffer-function #'my/difftastic-pop-at-bottom) #+end_src And I suspect the built-in window width function doesn't work as intended because of =global-display-line-numbers-mode=. #+begin_src emacs-lisp (setq difftastic-requested-window-width-function (lambda () (- (frame-width) 4))) #+end_src *** Editorconfig Editorconfig support for Emacs. References: - [[https://editorconfig.org/][Editorconfig reference]] #+begin_src emacs-lisp (use-package editorconfig :straight t :config (add-to-list 'editorconfig-indentation-alist '(emmet-mode emmet-indentation)) (editorconfig-mode)) #+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 :PROPERTIES: :MODULE_NAME: general-config :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-config.el :comments links :END: 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" #'consult-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 Keybindings: #+begin_src emacs-lisp (my-leader-def :infix "g" "G" #'consult-global-mark "g" #'consult-mark "C" #'my/mark-ring-clear "m" #'my/push-mark-no-activate) (general-define-key :keymaps 'global "C-SPC" #'my/push-mark-no-activate) #+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) "-" #'avy-goto-char-timer)) #+End_src Integrate avy with embark: #+begin_src emacs-lisp (defun avy-action-embark (pt) (unwind-protect (save-excursion (goto-char pt) (embark-act)) (select-window (cdr (ring-ref avy-ring 0)))) t) (with-eval-after-load 'avy (setf (alist-get ?. avy-dispatch-alist) 'avy-action-embark)) #+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 :PROPERTIES: :MODULE_NAME: general-config :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-config.el :comments links :END: *** vertico [[https://github.com/minad/vertico][vertico]] is a vertical completion framework. I switched to it from [[https://github.com/abo-abo/swiper][Ivy]] (and to Ivy from [[https://github.com/emacs-helm/helm][Helm]]). #+begin_src emacs-lisp (use-package vertico :straight t :config (setq enable-recursive-minibuffers t) (general-define-key :keymaps '(vertico-map) "M-j" #'vertico-next "M-k" #'vertico-previous "TAB" #'minibuffer-complete) (vertico-mode)) #+end_src Add prompt indicator to =completing-read-multiple=: #+begin_src emacs-lisp (defun crm-indicator (args) (cons (format "[CRM%s] %s" (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator) (car args)) (cdr args))) (with-eval-after-load 'crm (advice-add #'completing-read-multiple :filter-args #'crm-indicator)) #+end_src Persist history over Emacs restarts. #+begin_src emacs-lisp (use-package savehist :init (savehist-mode)) #+end_src *** vertico extensions Vertico has a lot of extensions. [[https://github.com/minad/vertico/blob/main/extensions/vertico-directory.el][vertico-directory]] simplifies directory navigation. #+begin_src emacs-lisp (use-package vertico-directory :after (vertico) :config (general-define-key :keymaps '(vertico-map) "RET" #'vertico-directory-enter "DEL" #'vertico-directory-delete-char) (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy)) #+end_src [[https://github.com/minad/vertico/blob/main/extensions/vertico-grid.el][vertico-grid]] enabled grid display. It is useful when there are no annotations in the completion buffer. #+begin_src emacs-lisp (use-package vertico-grid :after (vertico)) #+end_src [[https://github.com/minad/vertico/blob/main/extensions/vertico-multiform.el][vertico-multiform]] enables per-mode configuration. #+begin_src emacs-lisp (defun my/sort-directories-first (files) (setq files (vertico-sort-alpha files)) (nconc (seq-filter (lambda (x) (string-suffix-p "/" x)) files) (seq-remove (lambda (x) (string-suffix-p "/" x)) files))) (use-package vertico-multiform :after vertico :config (vertico-multiform-mode) (general-define-key :keymap 'vertico-multiform-map "M-b" #'vertico-multiform-buffer "M-g" #'vertico-multiform-grid) (setq vertico-multiform-categories '((file (vertico-sort-function . my/sort-directories-first)) (password-store-pass grid))) (setq vertico-multiform-commands '((eshell-atuin-history (vertico-sort-function . nil)) (my/index-nav (vertico-sort-function . nil)) (org-ql-view (vertico-sort-function . nil)) (my/consult-line (vertico-sort-function . nil)) (telega-msg-add-reaction grid)))) #+end_src [[https://github.com/minad/vertico/blob/main/extensions/vertico-quick.el][vertico-quick]] enables ivy-like bindings to choose candidates. #+begin_src emacs-lisp (use-package vertico-quick :after vertico :config (general-define-key :keymaps '(vertico-map) "M-q" #'vertico-quick-insert "C-q" #'vertico-quick-exit)) #+end_src *** orderless [[https://github.com/oantolin/orderless][orderless]] is a flexible completion style framework. Completion style refers to the way entries are filtered in the completion buffer. I used to use [[https://github.com/radian-software/prescient.el][prescient.el]] with Ivy; unlike prescient, orderless doesn't sort completion entries. #+begin_src emacs-lisp (use-package orderless :straight t :config (setq completion-styles '(orderless basic)) (setq completion-category-defaults nil) (setq completion-category-overrides '((file (styles partial-completion)))) (setq orderless-matching-styles '(orderless-literal orderless-initialism orderless-regexp))) #+end_src Disable orderless for company: #+begin_src emacs-lisp (defun company-completion-styles (capf-fn &rest args) (let ((completion-styles '(basic partial-completion))) (apply capf-fn args))) (with-eval-after-load 'company (advice-add 'company-capf :around #'company-completion-styles)) #+end_src *** consult [[https://github.com/minad/consult][consult]] provides various commands based on the =completing-read= API. #+begin_src emacs-lisp (use-package consult :straight t :config (setq consult-preview-excluded-files `("\\`/[^/|:]+:" ,(rx "html" eos)))) #+end_src *** marginalia [[https://github.com/minad/marginalia][marginalia]] provides annotations in the completion interface. #+begin_src emacs-lisp (use-package marginalia :straight t :config (marginalia-mode) (push '(projectile-find-file . file) marginalia-command-categories)) #+end_src *** embark [[https://github.com/oantolin/embark][embark]] provides minibuffer actions. #+begin_src emacs-lisp (use-package embark :straight t :commands (embark-act embark-dwim embark-bindings) :init (general-define-key "M-e" #'embark-act)) (use-package embark-consult :straight t :after (embark) :config (add-hook 'embark-collect-mode #'consult-preview-at-point-mode)) #+end_src Use =which-key= like indicator. Take from the [[https://github.com/oantolin/embark/wiki/Additional-Configuration#use-which-key-like-a-key-menu-prompt][Embark wiki]]. #+begin_src emacs-lisp (defun embark-which-key-indicator () "An embark indicator that displays keymaps using which-key. The which-key help message will show the type and value of the current target followed by an ellipsis if there are further targets." (lambda (&optional keymap targets prefix) (if (null keymap) (which-key--hide-popup-ignore-command) (which-key--show-keymap (if (eq (plist-get (car targets) :type) 'embark-become) "Become" (format "Act on %s '%s'%s" (plist-get (car targets) :type) (embark--truncate-target (plist-get (car targets) :target)) (if (cdr targets) "…" ""))) (if prefix (pcase (lookup-key keymap prefix 'accept-default) ((and (pred keymapp) km) km) (_ (key-binding prefix 'accept-default))) keymap) nil nil t (lambda (binding) (not (string-suffix-p "-argument" (cdr binding)))))))) (defun embark-hide-which-key-indicator (fn &rest args) "Hide the which-key indicator immediately when using the completing-read prompter." (which-key--hide-popup-ignore-command) (let ((embark-indicators (remq #'embark-which-key-indicator embark-indicators))) (apply fn args))) (with-eval-after-load 'embark (advice-add #'embark-completing-read-prompter :around #'embark-hide-which-key-indicator) (setq embark-indicators (delq #'embark-mixed-indicator embark-indicators)) (push #'embark-which-key-indicator embark-indicators)) #+end_src *** keybindings Setting up quick access to various completions. #+begin_src emacs-lisp (my-leader-def :infix "f" "" '(:which-key "various completions")' "b" #'persp-switch-to-buffer* "e" 'micromamba-activate "f" 'project-find-file "c" 'consult-yank-pop "a" 'consult-ripgrep "d" 'deadgrep) (general-define-key :states '(insert normal) "C-y" 'consult-yank-pop) (defun my/consult-line () (interactive) (if current-prefix-arg (call-interactively #'consult-line-multi) (consult-line nil t))) ;; (my-leader-def "SPC SPC" 'ivy-resume) (my-leader-def "s" 'my/consult-line) #+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 :PROPERTIES: :MODULE_NAME: general-config :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-config.el :comments links :END: - *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 :PROPERTIES: :MODULE_NAME: wakatime :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-wakatime.el :comments links :END: 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") :config (setq wakatime-ignore-exit-codes '(0 1 102 112)) (advice-add 'wakatime-init :after (lambda () (setq wakatime-cli-path (or (executable-find "wakatime-cli") (expand-file-name "~/bin/wakatime-cli"))))) (when (file-exists-p "~/.wakatime.cfg") (setq wakatime-api-key (string-trim (shell-command-to-string "awk '/api-key/{print $NF}' ~/.wakatime.cfg")))) ;; (setq wakatime-cli-path (executable-find "wakatime")) (global-wakatime-mode)) #+end_src *** ActivityWatch :PROPERTIES: :MODULE_NAME: activitywatch :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-activitywatch.el :comments links :END: #+begin_src emacs-lisp (use-package request :straight t :defer t) (use-package activity-watch-mode :straight t :config (global-activity-watch-mode)) #+end_src * UI settings ** General settings :PROPERTIES: :MODULE_NAME: general-ui :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-ui.el :comments links :END: *** 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 This adds a confirmation to avoid accidental Emacs closing. #+begin_src emacs-lisp (setq confirm-kill-emacs 'y-or-n-p) #+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) :commands (olivetti-mode) :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 :init (define-minor-mode keycast-mode "Keycast mode" :global t (if keycast-mode (progn (add-to-list 'global-mode-string '("" keycast-mode-line " ")) (add-hook 'pre-command-hook 'keycast--update t) ) (remove-hook 'pre-command-hook 'keycast--update) (setq global-mode-string (delete '("" keycast-mode-line " ") global-mode-string)))) :commands (keycast--update)) #+end_src ** Themes and colors :PROPERTIES: :MODULE_NAME: general-ui :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-ui.el :comments links :END: *** Theme packages My colorschemes of choice. #+begin_src emacs-lisp (use-package doom-themes :straight t ;; Not deferring becuase I want `doom-themes-visual-bell-config' :config (setq doom-themes-enable-bold t doom-themes-enable-italic t) ;; (if my/remote-server ;; (load-theme 'doom-gruvbox t) ;; (load-theme 'doom-palenight t)) (doom-themes-visual-bell-config) (setq doom-themes-treemacs-theme "doom-colors") (doom-themes-treemacs-config)) #+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)) ((string-match-p (rx bos "dark-") color-name) (or (doom-color color) (ct-edit-hsl-l-dec (my/doom-color (intern (substring color-name 5))) my/alpha-for-light))) (t (doom-color color))))))) #+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 dark-red dark-green dark-yellow dark-blue dark-magenta dark-cyan light-red light-green light-yellow light-blue light-magenta light-cyan light-white bg bg-alt fg fg-alt violet grey base0 base1 base2 base3 base4 base5 base6 base7 base8 border)) (defun my/test-colors () (interactive) (let ((buf (generate-new-buffer "*colors-test*"))) (with-current-buffer buf (insert (format "%-20s %-10s %-10s %-10s" "Color" "Doom" "Modus" "Ef") "\n") (cl-loop for color in my/test-colors-list do (insert (format "%-20s %-10s %-10s %-10s\n" (prin1-to-string color) (my/doom-color color) (my/modus-color color) (my/ef-color color)))) (special-mode) (rainbow-mode)) (switch-to-buffer buf))) #+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))))) ((eq color 'modeline) (or (my/color-value 'bg-mode-line-active) (my/color-value 'bg-mode-line) (if (my/light-p) (ct-edit-hsl-l-dec (my/color-value 'bg-alt) 10) (ct-edit-hsl-l-inc (my/color-value 'bg-alt) 15)))) ((my/doom-p) (my/doom-color color)) ((my/modus-p) (my/modus-color color)) ((my/ef-p) (my/ef-color color)))) #+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 'unspecified :foreground 'unspecified) (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 (if my/is-termux (progn (my/switch-theme 'modus-operandi-tinted)) (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 :PROPERTIES: :MODULE_NAME: general-ui :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-ui.el :comments links :END: *** 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 I switched to [[https://github.com/rainstormstudio/nerd-icons.el][nerd-icons]] from [[https://github.com/domtronn/all-the-icons.el][all-the-icons]]. Run =M-x all-the-icons-install-fonts= at first setup. #+begin_src emacs-lisp (use-package nerd-icons :straight t) #+end_src ** Text highlight :PROPERTIES: :MODULE_NAME: general-ui :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-ui.el :comments links :END: Highlight indent guides. I used [[https://github.com/DarthFennec/highlight-indent-guides][highlight-indent-guides]] before but [[https://github.com/jdtsmith/indent-bars][indent-bars]] seems to work better, and it doesn't break with =treesit-fold=. #+begin_src emacs-lisp (use-package indent-bars :straight (:host github :repo "jdtsmith/indent-bars") :if (display-graphic-p) :hook ((prog-mode . indent-bars-mode) (LaTeX-mode . indent-bars-mode)) :config (require 'indent-bars-ts) (setopt indent-bars-no-descend-lists t indent-bars-treesit-support t indent-bars-width-frac 0.3)) #+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 :PROPERTIES: :MODULE_NAME: doom-modeline :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-doom-modeline.el :comments links :END: 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) (when my/is-termux (setopt doom-modeline-icon nil)) :config (setq doom-modeline-minor-modes nil) (setq doom-modeline-irc nil) (setq doom-modeline-buffer-state-icon nil) (doom-modeline-mode 1)) #+end_src *** Doom Modeline as Tab Bar #+begin_src emacs-lisp (defun my/tab-bar-mode-line--format () (unless (derived-mode-p 'company-box-mode) (cl-letf (((symbol-function 'window-pixel-width) 'frame-pixel-width) ((symbol-function 'window-margins) (lambda (&rest _) (list nil)))) (let ((doom-modeline-window-width-limit nil) (doom-modeline--limited-width-p nil)) (format-mode-line '("%e" (:eval (doom-modeline-format--main)))))))) (defun my/hide-mode-line-if-only-window () (let* ((windows (window-list)) (hide-mode-line-p (length= windows 1))) (dolist (win windows) (with-current-buffer (window-buffer win) (unless (eq hide-mode-line-p hide-mode-line-mode) (hide-mode-line-mode (if hide-mode-line-p +1 -1))))))) (define-minor-mode my/tab-bar-mode-line-mode "Use tab-bar as mode line mode." :global t (if my/tab-bar-mode-line-mode (progn (tab-bar-mode +1) (setq tab-bar-format '(my/tab-bar-mode-line--format)) (set-face-attribute 'tab-bar nil :inherit 'mode-line) (add-hook 'window-configuration-change-hook #'my/hide-mode-line-if-only-window) (dolist (buf (buffer-list)) (with-current-buffer buf (doom-modeline-set-modeline 'minimal))) (doom-modeline-set-modeline 'minimal 'default) (dolist (frame (frame-list)) (with-selected-frame frame (my/hide-mode-line-if-only-window)) (when-let (cb-frame (company-box--get-frame frame)) (set-frame-parameter cb-frame 'tab-bar-lines 0))) (setenv "POLYBAR_BOTTOM" "false") (when (fboundp #'my/exwm-run-polybar) (my/exwm-run-polybar))) (tab-bar-mode -1) (setq tab-bar-format '(tab-bar-format-history tab-bar-format-tabs tab-bar-separator tab-bar-format-add-tab)) (set-face-attribute 'tab-bar nil :inherit 'default) (remove-hook 'window-configuration-change-hook #'my/hide-mode-line-if-only-window) (global-hide-mode-line-mode -1) (dolist (buf (buffer-list)) (with-current-buffer buf (doom-modeline-set-modeline 'main))) (doom-modeline-set-modeline 'main 'default) (setenv "POLYBAR_BOTTOM" "true") (when (fboundp #'my/exwm-run-polybar) (my/exwm-run-polybar)))) #+end_src ** perspective.el :PROPERTIES: :MODULE_NAME: perspective :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-perspective.el :comments links :END: [[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-switch-to-buffer "x" 'persp-switch-to-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 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 :PROPERTIES: :MODULE_NAME: treemacs :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-treemacs.el :comments links :END: [[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 :PROPERTIES: :MODULE_NAME: lsp :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-lsp.el :comments links :END: 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 :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) (setq lsp-volar-take-over-mode 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)) (use-package all-the-icons :straight t) #+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 **** Fix vue-semantic-server Somehow, for me [[https://github.com/vuejs/language-tools][vue-semantic-server]] sends an empty =textDocument/publishDiagnostics= message a second or two after the real one, causing the diagnostics to disappear. For now, I've advised this away by ignoring empty messages unless they show up more than 5 seconds after a non-empty message. #+begin_src emacs-lisp (setq my/lsp--vue-diagnostics-last-update (make-hash-table :test #'equal)) (defun my/lsp--on-diagnostics (fn workspace params) (if (equal (gethash 'vue-semantic-server lsp-clients) (lsp--workspace-client workspace)) (progn (let* ((is-empty (seq-empty-p (gethash "diagnostics" params))) (uri (gethash "uri" params)) (last-update (gethash uri my/lsp--vue-diagnostics-last-update)) (current-update (time-convert nil #'integer))) (unless is-empty (puthash uri current-update my/lsp--vue-diagnostics-last-update)) (when (or (not is-empty) (not last-update) (> (- current-update (or last-update 0)) 5)) (funcall fn workspace params)))) (funcall fn workspace params))) (with-eval-after-load 'lsp (advice-add #'lsp--on-diagnostics :around #'my/lsp--on-diagnostics)) #+end_src *** Flycheck :PROPERTIES: :MODULE_NAME: flycheck :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-flycheck.el :comments links :END: 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)) (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 *** Tree-sitter :PROPERTIES: :MODULE_NAME: tree-sitter :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-tree-sitter.el :comments links :END: References: - [[https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax][Tree sitter query syntax]] - [[https://www.gnu.org/software/emacs//manual/html_node/elisp/Parsing-Program-Source.html][treesit.el documentation]] **** treesit.el 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 **** treesit-fold [[https://github.com/emacs-tree-sitter/treesit-fold][treesit-fold]] uses treesit.el for folding. #+begin_src emacs-lisp (use-package treesit-fold :straight (treesit-fold :type git :host github :repo "emacs-tree-sitter/treesit-fold") :commands (treesit-fold-mode)) #+end_src ***** Hide children of current node A function to hide children of the current foldable node. This is somewhat similar to =hs-hide-level=, which I got used to. First, find all foldable nodes: #+begin_src emacs-lisp (defun my/treesit-fold--get-nodes-to-fold () (when-let* ((node (ignore-errors (treesit-buffer-root-node))) (patterns (seq-mapcat (lambda (fold-range) `((,(car fold-range)) @name)) (alist-get major-mode treesit-fold-range-alist))) (query (ignore-errors (treesit-query-compile (treesit-node-language node) patterns))) (nodes-to-fold (treesit-query-capture node query)) (mode-ranges (alist-get major-mode treesit-fold-range-alist)) (nodes-to-fold (cl-remove-if (lambda (node) (treesit-fold--non-foldable-node-p (cdr node) mode-ranges)) nodes-to-fold))) nodes-to-fold)) #+end_src Then filter children of the current foldable node and fold them: #+begin_src emacs-lisp (defun my/treesit-fold-hide-children () (interactive) (let* ((current-node (treesit-fold--foldable-node-at-pos)) (all-nodes-to-fold (my/treesit-fold--get-nodes-to-fold)) ;; Find foldable children of `current-node' (target-nodes-to-fold (seq-filter (lambda (n) (cl-block tree-iter (while n (setq n (treesit-node-parent n)) (when (equal n current-node) (cl-return-from tree-iter t))))) (mapcar #'cdr all-nodes-to-fold)))) (dolist (node target-nodes-to-fold) (treesit-fold-close node)))) #+end_src #+begin_src emacs-lisp (defun my/evil-fold-hide-level () (interactive) (cond (hs-minor-mode (hs-hide-level)) (treesit-fold-mode (my/treesit-fold-hide-children)))) #+end_src #+begin_src emacs-lisp (with-eval-after-load 'treesit-fold (general-define-key :states '(normal) "ze" #'my/evil-fold-hide-level) (keymap-unset evil-motion-state-map "z e" t)) #+end_src **** combobulate [[https://github.com/mickeynp/combobulate][combobulate]] is Mickey Peterson's package that uses =tree-sitter= for structural navigation. I'm not actually using that, but its query editor is pretty useful to debug =tree-sitter= queries. #+begin_src emacs-lisp (use-package combobulate :straight (:host github :repo "mickeynp/combobulate") :commands (combobulate)) #+end_src *** DAP :PROPERTIES: :MODULE_NAME: dap :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-dap.el :comments links :END: 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 :PROPERTIES: :MODULE_NAME: reformatter :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-reformatter.el :comments links :END: 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 :PROPERTIES: :MODULE_NAME: copilot :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-copilot.el :comments links :END: [[https://copilot.github.com/][GitHub Copilot]] is a project of GitHub and OpenAI that provides code completions. | Type | Note | |--------+-----------------------------------------------| | *TODO* | Figure out how to bypass U.S. export controls | #+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 treesit-fold-mode outline-minor-mode)) (evil-toggle-fold) t) (indent-for-tab-command))) (use-package copilot :straight (:host github :repo "copilot-emacs/copilot.el") :commands (copilot-mode) :disabled t :init (add-hook 'emacs-startup-hook (lambda () (add-hook 'prog-mode-hook #'copilot-mode))) :config (push '(copilot) warning-suppress-types) (setq copilot-node-executable "/home/pavel/.guix-extra-profiles/dev/dev/bin/node") (general-define-key :keymaps 'company-active-map "" #'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)) #+end_src ** Web development :PROPERTIES: :MODULE_NAME: web :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-web.el :comments links :END: 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 #'treesit-fold-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 #'treesit-fold-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) (with-eval-after-load 'editorconfig (push 'standard-indent (alist-get 'web-mode editorconfig-indentation-alist))) (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-file-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 _) (let ((filename (buffer-file-name))) (when (and (stringp filename) (string-match-p (rx ".vue" eos) filename)) (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 :PROPERTIES: :MODULE_NAME: latex :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-latex.el :comments links :END: *** 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 :PROPERTIES: :MODULE_NAME: markup :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-markup.el :comments links :END: *** 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 :mode (rx (| ".asciidoc") eos) :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)) ;; (cons (rx (| "srt" "vtt" "ass") eos) #'subed-mode) :mode ("\\(?:ass\\|\\(?:sr\\|vt\\)t\\)\\'" . subed-mode) :config (general-define-key :keymaps '(subed-mode-map subed-vtt-mode-map) :states '(normal) "gp" #'subed-mpv-toggle-pause)) #+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-GB" "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 (my/is-termux nil) ((null file-name) nil) ((string-match-p (rx "/home/pavel/" (+ alnum) ".org" eos) file-name) nil) ((string-match-p (rx (literal org-directory) "/" (or "roam" "inbox-notes" "literature-notes" "journal")) file-name) t) ((string-match-p (rx (literal org-directory)) file-name) nil) ((string-match-p (rx (literal (expand-file-name user-emacs-directory))) file-name) nil) (t t)))) #+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-6.4/languagetool-server.jar") (setq langtool-mother-tongue "ru") (setq langtool-default-language "ru-RU")) (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) :commands (reverso) :config (setq reverso-languages '(russian english german)) (reverso-history-mode)) #+end_src ** Lisp :PROPERTIES: :MODULE_NAME: lisp :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-lisp.el :comments links :END: [[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 :defer t :init (defun my/flycheck-package-setup () (require 'flycheck-package) (flycheck-package-setup) (remove-hook 'emacs-lisp-mode-hook #'my/flycheck-package-setup)) (add-hook 'emacs-lisp-mode-hook #'my/flycheck-package-setup)) #+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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** 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 :commands (ein:run) :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 :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 #'treesit-fold-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) :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) (unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p)) (py-isort-buffer)) (python-black-buffer))) #+end_src *** OFF sphinx-doc CLOSED: [2024-04-23 Tue 12:33] A package to generate sphinx-compatible docstrings. #+begin_src emacs-lisp :tangle no (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 *** numpydoc [[https://github.com/douglasdavis/numpydoc.el][numpydoc.el]] is a package to generate docstring in Python functions. #+begin_src emacs-lisp (use-package numpydoc :straight t :commands (numpydoc-generate) :init (my-leader-def :keymaps 'python-ts-mode-map "rd" #'numpydoc-generate) :config (setq numpydoc-insertion-style 'prompt) (setq numpydoc-insert-return-without-typehint nil)) #+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 python-ts-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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** JSON #+begin_src emacs-lisp (use-package json-mode :straight t :mode "\\.json\\'" :config (add-hook 'json-mode-hook #'smartparens-mode) (add-hook 'json-mode-hook #'treesit-fold-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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** .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 :mode "Jenkinsfile\\'" :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 :mode "/crontab\\(\\.X*[[:alnum:]]+\\)?\\'" :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 :mode "\\.hcl\\'" :straight t) #+end_src ** Shell :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** 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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** 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 :mode "\\.sparql\\'" :straight t) #+end_src *** GraphQL #+begin_src emacs-lisp (use-package graphql-mode :mode (rx (| "gql" "grapql") eos) :straight t) #+end_src ** Documents :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** 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 ** Gnuplot :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: Emacs integration for [[http://gnuplot.info/][gnuplot]]. #+begin_src emacs-lisp (use-package gnuplot :straight t :commands (gnuplot-mode gnuplot-make-buffer) :init (add-to-list 'auto-mode-alist '("\\.gp\\'" . gnuplot-mode)) :config (general-define-key :keymaps 'gnuplot-mode-map "C-c C-c" #'gnuplot-send-buffer-to-gnuplot) (general-define-key :states '(normal) :keymaps 'gnuplot-mode-map "RET" #'gnuplot-send-buffer-to-gnuplot) (add-hook 'gnuplot-mode-hook #'smartparens-mode)) #+end_src ** x509 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+begin_src emacs-lisp (use-package x509-mode :commands (x509-dwim) :straight (:host github :repo "jobbflykt/x509-mode" :build (:not native-compile))) #+end_src ** Java :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+begin_src emacs-lisp (use-package lsp-java :straight t :after (lsp) :config (setq lsp-java-jdt-download-url "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/1.23.0/jdt-language-server-1.23.0-202304271346.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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+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 #'treesit-fold-mode)) #+end_src ** .NET :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: *** 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 #'treesit-fold-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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+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 :PROPERTIES: :MODULE_NAME: misc-programming :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-misc-programming.el :comments links :END: #+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 :PROPERTIES: :MODULE_NAME: general-org :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-general-org.el :comments links :END: *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 (setq org-directory (expand-file-name "~/30-39 Life/32 org-mode")) (use-package org :straight (:type built-in) :defer t :init (unless (file-exists-p org-directory) (mkdir org-directory t)) :config (setq org-startup-indented (not my/is-termux)) (setq org-return-follows-link t) (setq org-src-tab-acts-natively nil) (add-hook 'org-mode-hook 'smartparens-mode) (add-hook 'org-agenda-mode-hook (lambda () (visual-line-mode -1) (toggle-truncate-lines 1) (display-line-numbers-mode 0))) (add-hook 'org-mode-hook (lambda () (rainbow-delimiters-mode -1)))) #+end_src *** Encryption Setting up =org-crypt= to encrypt parts of file. #+begin_src emacs-lisp :noweb-ref (with-eval-after-load 'org (require 'org-crypt) (org-crypt-use-before-save-magic) (setq org-tags-exclude-from-inheritance '("crypt")) (setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205")) #+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 '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 *** 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 (defun my/outline-prev-or-up-heading () (interactive) (if (outline-on-heading-p) (outline-up-heading 1) (outline-previous-visible-heading 1))) (with-eval-after-load 'org (general-define-key :keymaps 'org-mode-map "C-c d" #'org-decrypt-entry "C-c e" #'org-encrypt-entry "M-p" #'org-latex-preview "M-o" #'org-redisplay-inline-images) (general-define-key :keymaps 'org-mode-map :states '(normal emacs) "L" #'org-shiftright "H" #'org-shiftleft "S-" #'org-next-visible-heading "S-" #'org-previous-visible-heading "M-0" #'org-next-visible-heading "M-9" #'org-previous-visible-heading "C-0" #'org-forward-heading-same-level "C-9" #'org-backward-heading-same-level "(" #'my/outline-prev-or-up-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 '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" "learning") (mapcar (lambda (f) (directory-files (concat org-directory "/" f) t (rx ".org" eos)))) (apply #'append) (mapcar (lambda (file) (string-replace (concat org-directory "/") "" file))) (append '("inbox.org" "contacts.org" "recurring.org"))))) (find-file (concat org-directory "/" (completing-read "Org file: " files))))) #+end_src ** UI *** LaTeX fragments A function to enable LaTeX native highlighting. Not setting this as default, because it loads LaTeX stuff. #+begin_src emacs-lisp (defun my/enable-org-latex () (interactive) (customize-set-variable 'org-highlight-latex-and-related '(native)) (add-hook 'org-mode-hook (lambda () (yas-activate-extra-mode 'LaTeX-mode))) (sp-local-pair 'org-mode "$" "$") (sp--remove-local-pair "'")) #+end_src Call the function before opening an org file or reopen a buffer after calling the function. Scale latex fragments preview. #+begin_src emacs-lisp (with-eval-after-load 'org (setq my/org-latex-scale 1.75) (setq org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale))) #+end_src Also, LaTeX fragments preview tends to break whenever the are custom =#+LATEX_HEADER= entries. To circumvent this, I add a custom header and modify the ~org-preview-latex-process-alist~ variable #+begin_src emacs-lisp (with-eval-after-load 'org (setq my/latex-preview-header "\\documentclass{article} \\usepackage[usenames]{color} \\usepackage{graphicx} \\usepackage{grffile} \\usepackage{longtable} \\usepackage{wrapfig} \\usepackage{rotating} \\usepackage[normalem]{ulem} \\usepackage{amsmath} \\usepackage{textcomp} \\usepackage{amssymb} \\usepackage{capt-of} \\usepackage{hyperref} \\pagestyle{empty}") (setq org-preview-latex-process-alist (mapcar (lambda (item) (cons (car item) (plist-put (cdr item) :latex-header my/latex-preview-header))) org-preview-latex-process-alist))) #+end_src *** Better headers [[https://github.com/integral-dw/org-superstar-mode][org-superstar-mode]] is a package that makes Org heading lines look a bit prettier. Disabled it for now because of overlapping functionality with org-bars. #+begin_src emacs-lisp (use-package org-superstar :straight t :disabled :hook (org-mode . org-superstar-mode)) #+end_src [[https://github.com/tonyaldon/org-bars][org-bars]] highlights Org indentation with bars. #+begin_src emacs-lisp (use-package org-bars :straight (:repo "tonyaldon/org-bars" :host github) :if (display-graphic-p) :hook (org-mode . org-bars-mode)) #+end_src Fallback to the standard =org-indent-mode= on terminal. #+begin_src emacs-lisp (unless (display-graphic-p) (add-hook 'org-mode-hook #'org-indent-mode)) #+end_src Remove the ellipsis at the end of folded headlines, as it seems unnecessary with =org-bars=. #+begin_src emacs-lisp (defun my/org-no-ellipsis-in-headlines () (remove-from-invisibility-spec '(outline . t)) (add-to-invisibility-spec 'outline)) (with-eval-after-load 'org-bars (add-hook 'org-mode-hook #'my/org-no-ellipsis-in-headlines) (when (eq major-mode 'org-mode) (my/org-no-ellipsis-in-headlines))) #+end_src *** Override colors #+begin_src emacs-lisp (my/use-colors (org-block :background (my/color-value 'bg-other)) (org-block-begin-line :background (my/color-value 'bg-other) :foreground (my/color-value 'grey))) #+end_src *** Hide stuff in buffer [[https://github.com/awth13/org-appear][org-appear]] is a package that toggles visibility of hidden elements upon entering and leaving them. #+begin_src emacs-lisp (use-package org-appear :after (org) :straight t) #+end_src [[https://github.com/io12/org-fragtog][org-fragtog]] does the same for LaTeX fragment previews. #+begin_src emacs-lisp (use-package org-fragtog :after (org) :straight t) #+end_src ** Literate programing :PROPERTIES: :MODULE_NAME: org-literate :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-org-literate.el :comments links :END: *** 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 A function to load =jupyter=. The problem with doing that on startup is that it tried to locate the =jupyter= executable, which I have only in an environment. #+begin_src emacs-lisp (defun my/org-load-jupyter () (interactive) (org-babel-do-load-languages 'org-babel-load-languages '((jupyter . t))) (my/jupyter-refesh-langs)) #+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 :mode ("\\.http\\'" . restclient-mode) :config (general-define-key :keymaps 'restclient-mode-map :states '(normal visual) "RET" #'restclient-http-send-current "M-RET" #'restclient-http-send-current-stay-in-window "y" nil "M-y" #'restclient-copy-curl-command) (general-define-key :keymaps 'restclient-response-mode-map :states '(normal visual) "q" #'quit-window)) (use-package ob-restclient :after (org restclient) :if (not my/remote-server) :straight t) #+end_src *** Org Babel Setup Enable languages #+begin_src emacs-lisp (with-eval-after-load 'org (org-babel-do-load-languages 'org-babel-load-languages `((emacs-lisp . t) (python . t) (sql . t) (sqlite . t) ;; (typescript .t) (hy . t) (shell . t) (plantuml . t) (octave . t) (sparql . t) (gnuplot . t))) (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)) #+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 Execute all code blocks marked with =:startup t=: #+begin_src emacs-lisp (defun my/org-babel-execute-marked (&optional arg) (interactive "P") (let (markers) (org-element-map (org-element-parse-buffer) 'src-block (lambda (elem) (let ((params (org-element-property :parameters elem))) (when (and params (string-match-p (rx "startup t") params)) (let ((m (make-marker))) (set-marker m (org-element-property :begin elem)) (set-marker-insertion-type m t) (push m markers)))))) (setq markers (nreverse markers)) (when arg (setq markers (seq-filter (lambda (m) (> (marker-position m) (point))) markers))) (dolist (m markers) (goto-char m) (ignore-errors (org-babel-execute-src-block))))) #+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 :commands (hide-mode-line-mode)) (defun my/present-next-with-latex () (interactive) (org-present-next) (org-latex-preview '(16))) (defun my/present-prev-with-latex () (interactive) (org-present-prev) (org-latex-preview '(16))) (use-package org-present :straight (:host github :repo "rlister/org-present") :if (not my/remote-server) :commands (org-present) :config (general-define-key :keymaps 'org-present-mode-keymap "" '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 *** 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 :config (setq org-attach-screenshot-auto-refresh 'never)) #+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 *** 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 :PROPERTIES: :MODULE_NAME: org-productivity :header-args:emacs-lisp: :tangle /home/pavel/.emacs.d/modules/sqrt-org-productivity.el :comments links :END: 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 (when (file-directory-p (concat org-directory "/projects")) (thread-last "/projects" (concat org-directory) (directory-files) (mapcar (lambda (f) (concat org-directory "/projects/" f))) (seq-filter (lambda (f) (not (file-directory-p f)))))))) (setq org-agenda-files (seq-filter #'file-exists-p (append project-files (mapcar (lambda (f) (concat org-directory "/" f)) '("inbox.org" "misc/habit.org" "contacts.org"))))) (setq org-refile-targets `(,@(mapcar (lambda (f) `(,f . (:tag . "refile"))) project-files) ,@(mapcar (lambda (f) `(,f . (:regexp . "Tasks"))) project-files))) (when (file-exists-p (concat org-directory "/scripts/refile.el")) (load-file (concat org-directory "/scripts/refile.el")) (run-hooks 'my/org-refile-hooks)))) (setq org-roam-directory (concat org-directory "/roam")) (with-eval-after-load 'org (require 'seq) (my/update-org-agenda)) #+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)) :config (setq org-clock-agg-node-format "%-%(+ title-width)t %20c %8z %s/%S") (setq org-clock-agg-node-title-width-delta 47) (push (cons "Agenda+Archive" (append (org-agenda-files) (thread-last "/projects/archive" (concat org-directory) (directory-files) (mapcar (lambda (f) (concat org-directory "/projects/archive/" f))) (seq-filter (lambda (f) (not (file-directory-p f))))))) org-clock-agg-files-preset)) #+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 '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 ***** Fix tasks without TASK_KIND #+begin_src emacs-lisp (defun my/org-fix-task-kind () (interactive) (let ((entries (org-ql-query :select #'element-with-markers :from (current-buffer) :where '(and (olp "Tasks") (not (property "TASK_KIND")) (clocked))))) (org-fold-show-all) (dolist (entry entries) (let ((marker (org-element-property :org-marker entry))) (org-with-point-at marker (let ((value (org-read-property-value "TASK_KIND"))) (org-set-property "TASK_KIND" value))))))) #+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 :config (setq org-ql-ask-unsafe-queries nil) :init ;; See https://github.com/alphapapa/org-ql/pull/237 (setq org-ql-regexp-part-ts-time (rx " " (repeat 1 2 digit) ":" (repeat 2 digit) (optional "-" (repeat 1 2 digit) ":" (repeat 2 digit)))) (my-leader-def :infix "o" "v" #'org-ql-view "q" #'org-ql-search)) #+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)) (vertico-sort-function nil) (categories (completing-read-multiple "Categories: " '("TEACH" "EDU" "JOB" "LIFE" "COMP")))) (org-ql-search (org-agenda-files) `(and (todo) ,@(unless (seq-empty-p categories) `((category ,@categories)))) :sort '(priority todo deadline) :super-groups '((:auto-outline-path-file t))))) #+end_src ***** Items clocked or closed today Some custom functions to account for =org-extend-today-until=. Needed because sometimes my daily reviews cross 00:00. #+begin_src emacs-lisp (defun my/org-ql-clocked-today () (interactive) (let ((today (format-time-string "%Y-%m-%d" (days-to-time (- (org-today) (time-to-days 0)))))) (org-ql-search (org-agenda-files) `(clocked :from ,today) :title "Clocked today" :sort '(todo priority date) :super-groups '((:auto-outline-path-file t) (:auto-todo t))))) #+end_src #+begin_src emacs-lisp (defun my/org-ql-closed-today () (interactive) (let ((today (format-time-string "%Y-%m-%d" (days-to-time (- (org-today) (time-to-days 0)))))) (org-ql-search (org-agenda-files) `(closed :from ,today) :title "Closed today" :sort '(todo priority date) :super-groups '((:auto-outline-path-file t) (:auto-todo 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 (tags "nots")) (not (ts :from -14))) :title "Review: Stale tasks" :sort '(todo priority date) :super-groups '((:auto-outline-path-file t)))) (cons "Review: Unclocked tasks" (list :buffers-files #'org-agenda-files :query '(and (done) (ts :from -14) (not (clocked)) (not (tags "nots"))) :title "Review: Unclocked tasks" :sort '(todo priority date) :super-groups '((:auto-outline-path-file t)))) (cons "Review: Recently timestamped" #'my/org-ql-view-recent-items) (cons "Review: Clocked today" #'my/org-ql-clocked-today) (cons "Review: Closed today" #'my/org-ql-closed-today) (cons "Fix: tasks without TASK_KIND" (lambda () (interactive) (org-ql-search (current-buffer) '(and (olp "Tasks") (not (property "TASK_KIND")) (clocked)) :super-groups '((:auto-outline-path-file t))))))) #+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 **** 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. There's also [[https://gitlab.com/grinn.amy/org-yaap][org-yaap]] by Amy Grinn, but I opt for my system for now. #+begin_src emacs-lisp (use-package org-yaap :straight (org-yaap :type git :host gitlab :repo "SqrtMinusOne/org-yaap") :after (org) :if (not my/nested-emacs) :disabled t :config (org-yaap-mode 1) (setq org-yaap-alert-before '(10 1)) (setq org-yaap-alert-title "PROXIMITY ALERT") (setq org-yaap-todo-keywords-only '("FUTURE"))) #+end_src 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 (