#+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 * Introduction My configuration of [[https://www.gnu.org/software/emacs/][GNU Emacs]], an awesome +text editor+ program that can do almost anything. At the moment of this writing, this "almost anything" includes: - *Writing code*. With LSP & Co this functionality of Emacs may rival that of IDEs, and is at least on par with editors like VS Code.\\ One thing where Emacs is particularly good is writing Lisp code, e.g. Clojure, Common Lisp, and, of course, Emacs Lisp. - *Literate programming* with Org Mode. That includes: - Configuring the entirety of my software that can be configured with text files. - Interactive programming like one provided by Jupyter Notebook. - *File management*. Dired is my primary file manager. - *Email*, with notmuch. - *Multimedia management*, with EMMS. - *RSS feed reader*, with elfeed. - *Task management*, with Org Mode. - *Managing passwords*, with pass. - *IRC*, with ERC. - *Formatting documents*, also with Org Mode. When the document is too complex, I prefer to write plain LaTeX, but I've come to the conclusion that in most cases Org Mode covers my needs there. - *X Window management*, with EXWM. So I could say I literally live in Emacs. - ... As I have hinted above, this file is a piece of literate configuration, which means that the actual code is interweaved with English-language commentary. One could argue that the commentary, not the code, is the primary entity of the file. But at the same time, the configuration is personal, so in fact, the primary benefactor of the literate structure is me. The commentary is primarily meant to capture my state of mind at the moment of writing the code, which is immensely helpful for maintaining the code in the future. Besides, I can't say I'm stable with it. Occasionally I save some promising experimentations from scratch buffers without much comment. Or I may not have enough time to describe things in substantial detail. Or, as it is at the moment when I'm writing this, I have the time to write down whatever I consider necessary. Or, when I'm writing a blog post about Emacs configuration, I usually incorporate some things back into this config. And of course, human minds share many similarities, so if you are an avid Emacs user, you probably can extract something of value from here. Although in this case, your configuration has a decent chance to have everything I'm doing here. But who knows, because somehow mine is over 6000 LoC at the moment, despite being just a bit over a year old. If however, by some twist of fate, this document is one of the first things you see about Emacs, it won't be a good resource for you. And you definitely shouldn't try to launch this config as it is. If I could suggest only one resource, I'd advise David Wilson's [[https://www.youtube.com/c/SystemCrafters][System Crafters]] YouTube channel. #+TOC: headlines 6 * Contents :noexport: :PROPERTIES: :TOC: :include all :depth 4 :END: :CONTENTS: - [[#some-remarks][Some remarks]] - [[#bootstrap][Bootstrap]] - [[#packages][Packages]] - [[#straightel][straight.el]] - [[#use-package][use-package]] - [[#variables--environment][Variables & environment]] - [[#performance][Performance]] - [[#measure-startup-speed][Measure startup speed]] - [[#garbage-collection][Garbage collection]] - [[#run-garbage-collection-when-emacs-is-unfocused][Run garbage collection when Emacs is unfocused]] - [[#native-compilation][Native compilation]] - [[#anaconda][Anaconda]] - [[#config-files][Config files]] - [[#custom-file-location][Custom file location]] - [[#private-config][Private config]] - [[#no-littering][No littering]] - [[#prevent-emacs-from-closing][Prevent Emacs from closing]] - [[#general-settings][General settings]] - [[#keybindings][Keybindings]] - [[#generalel][general.el]] - [[#which-key][which-key]] - [[#dump-keybindings][dump keybindings]] - [[#evil][Evil]] - [[#evil-mode][Evil-mode]] - [[#addons][Addons]] - [[#evil-collection][evil-collection]] - [[#my-keybindings][My keybindings]] - [[#escape-key][Escape key]] - [[#home--end][Home & end]] - [[#my-leader][My leader]] - [[#universal-argument][Universal argument]] - [[#profiler][Profiler]] - [[#buffer-switching][Buffer switching]] - [[#buffer-management][Buffer management]] - [[#xref][xref]] - [[#folding][Folding]] - [[#zoom-ui][Zoom UI]] - [[#i3-integration][i3 integration]] - [[#editing-text][Editing text]] - [[#indentation--whitespace][Indentation & whitespace]] - [[#aggressive-indent][Aggressive Indent]] - [[#delete-trailing-whitespace][Delete trailing whitespace]] - [[#tabs][Tabs]] - [[#settings][Settings]] - [[#scrolling][Scrolling]] - [[#clipboard][Clipboard]] - [[#backups][Backups]] - [[#undo-tree][Undo Tree]] - [[#snippets][Snippets]] - [[#other-small-packages][Other small packages]] - [[#managing-parentheses-smartparens][Managing parentheses (smartparens)]] - [[#expand-region][Expand region]] - [[#visual-fill-column-mode][Visual fill column mode]] - [[#working-with-projects][Working with projects]] - [[#treemacs][Treemacs]] - [[#helper-functions][Helper functions]] - [[#custom-icons][Custom icons]] - [[#projectile][Projectile]] - [[#git--magit][Git & Magit]] - [[#editorconfig][Editorconfig]] - [[#completion][Completion]] - [[#ivy-counsel-swiper][Ivy, counsel, swiper]] - [[#ivy-rich][ivy-rich]] - [[#prescient][prescient]] - [[#keybindings][keybindings]] - [[#company][company]] - [[#help][Help]] - [[#time-trackers][Time trackers]] - [[#wakatime][WakaTime]] - [[#activitywatch][ActivityWatch]] - [[#ui-settings][UI settings]] - [[#general-settings][General settings]] - [[#miscellaneous][Miscellaneous]] - [[#line-numbers][Line numbers]] - [[#word-wrapping][Word wrapping]] - [[#custom-frame-format][Custom frame format]] - [[#themes-and-colors][Themes and colors]] - [[#dim-inactive-buffers][Dim inactive buffers]] - [[#doom-themes][Doom themes]] - [[#custom-theme][Custom theme]] - [[#fonts][Fonts]] - [[#frame-font][Frame font]] - [[#ligatures][Ligatures]] - [[#icons][Icons]] - [[#text-highlight][Text highlight]] - [[#doom-modeline][Doom Modeline]] - [[#perspectiveel][perspective.el]] - [[#some-functions][Some functions]] - [[#programming][Programming]] - [[#general-setup][General setup]] - [[#lsp][LSP]] - [[#setup][Setup]] - [[#integrations][Integrations]] - [[#keybindings][Keybindings]] - [[#flycheck][Flycheck]] - [[#tree-sitter][Tree Sitter]] - [[#dap][DAP]] - [[#controls][Controls]] - [[#ui-fixes][UI Fixes]] - [[#helper-functions][Helper functions]] - [[#improved-stack-frame-switching][Improved stack frame switching]] - [[#debug-templates][Debug templates]] - [[#off-tabnine][(OFF) TabNine]] - [[#reformatter][Reformatter]] - [[#general-additional-config][General additional config]] - [[#web-development][Web development]] - [[#emmet][Emmet]] - [[#prettier][Prettier]] - [[#typescript][TypeScript]] - [[#javascript][JavaScript]] - [[#jest][Jest]] - [[#web-mode][web-mode]] - [[#scss][SCSS]] - [[#php][PHP]] - [[#latex][LaTeX]] - [[#auctex][AUCTeX]] - [[#bibtex][BibTeX]] - [[#import-sty][Import *.sty]] - [[#snippets][Snippets]] - [[#greek-letters][Greek letters]] - [[#english-letters][English letters]] - [[#math-symbols][Math symbols]] - [[#section-snippets][Section snippets]] - [[#other-markup-languages][Other markup languages]] - [[#markdown][Markdown]] - [[#plantuml][PlantUML]] - [[#languagetool][LanguageTool]] - [[#lisp][Lisp]] - [[#meta-lisp][Meta Lisp]] - [[#emacs-lisp][Emacs Lisp]] - [[#package-lint][Package Lint]] - [[#general-settings][General settings]] - [[#common-lisp][Common lisp]] - [[#slime][SLIME]] - [[#general-settings][General settings]] - [[#clojure][Clojure]] - [[#hy][Hy]] - [[#scheme][Scheme]] - [[#clips][CLIPS]] - [[#python][Python]] - [[#pipenv][pipenv]] - [[#yapf][yapf]] - [[#isort][isort]] - [[#sphinx-doc][sphinx-doc]] - [[#pytest][pytest]] - [[#fix-comint-buffer-width][Fix comint buffer width]] - [[#code-cells][code-cells]] - [[#tensorboard][tensorboard]] - [[#java][Java]] - [[#go][Go]] - [[#net][.NET]] - [[#c][C#]] - [[#msbuild][MSBuild]] - [[#fish][fish]] - [[#sh][sh]] - [[#haskell][Haskell]] - [[#lua][Lua]] - [[#json][JSON]] - [[#sql][SQL]] - [[#sparql][SPARQL]] - [[#yaml][YAML]] - [[#env][.env]] - [[#csv][CSV]] - [[#docker][Docker]] - [[#crontab][crontab]] - [[#org-mode][Org Mode]] - [[#installation--basic-settings][Installation & basic settings]] - [[#encryption][Encryption]] - [[#org-contrib][org-contrib]] - [[#integration-with-evil][Integration with evil]] - [[#literate-programing][Literate programing]] - [[#python--jupyter][Python & Jupyter]] - [[#hy][Hy]] - [[#view-html-in-browser][View HTML in browser]] - [[#plantuml][PlantUML]] - [[#setup][Setup]] - [[#managing-jupyter-kernels][Managing Jupyter kernels]] - [[#do-not-wrap-the-output-in-emacs-jupyter][Do not wrap the output in emacs-jupyter]] - [[#wrap-source-code-output][Wrap source code output]] - [[#managing-a-literate-programming-project][Managing a literate programming project]] - [[#productivity--knowledge-management][Productivity & Knowledge management]] - [[#capture-templates--various-settings][Capture templates & various settings]] - [[#trello-sync][Trello sync]] - [[#org-ql][org-ql]] - [[#custom-agendas][Custom agendas]] - [[#review-workflow][Review workflow]] - [[#data-from-git--org-roam][Data from git & org-roam]] - [[#data-from-org-journal][Data from org-journal]] - [[#data-from-org-agenda-via-org-ql][Data from org-agenda via org-ql]] - [[#capture-template][Capture template]] - [[#org-journal][Org Journal]] - [[#org-roam][Org Roam]] - [[#org-roam-ui][org-roam-ui]] - [[#org-roam-protocol][org-roam-protocol]] - [[#org-ref][org-ref]] - [[#org-roam-bibtex][org-roam-bibtex]] - [[#managing-tables][Managing tables]] - [[#ui][UI]] - [[#off-instant-equations-preview][(OFF) Instant equations preview]] - [[#latex-fragments][LaTeX fragments]] - [[#better-headers][Better headers]] - [[#org-agenda-icons][Org Agenda Icons]] - [[#export][Export]] - [[#general-settings][General settings]] - [[#hugo][Hugo]] - [[#jupyter-notebook][Jupyter Notebook]] - [[#html-export][Html export]] - [[#latex][LaTeX]] - [[#keybindings--stuff][Keybindings & stuff]] - [[#copy-a-link][Copy a link]] - [[#open-a-file-from-org-directory][Open a file from org-directory]] - [[#presentations][Presentations]] - [[#tools][Tools]] - [[#toc][TOC]] - [[#screenshots][Screenshots]] - [[#system-configuration][System configuration]] - [[#tables-for-guix-dependencies][Tables for Guix Dependencies]] - [[#noweb-evaluations][Noweb evaluations]] - [[#yadm-hook][yadm hook]] - [[#applications][Applications]] - [[#dired][Dired]] - [[#basic-config--keybindings][Basic config & keybindings]] - [[#addons][Addons]] - [[#subdirectories][Subdirectories]] - [[#tramp][TRAMP]] - [[#bookmarks][Bookmarks]] - [[#shells][Shells]] - [[#vterm][vterm]] - [[#configuration][Configuration]] - [[#subterminal][Subterminal]] - [[#dired-integration][Dired integration]] - [[#with-editor-integration][With-editor integration]] - [[#eshell][Eshell]] - [[#managing-dotfiles][Managing dotfiles]] - [[#open-emacs-config][Open Emacs config]] - [[#open-magit-for-yadm][Open Magit for yadm]] - [[#open-a-dotfile][Open a dotfile]] - [[#internet--multimedia][Internet & Multimedia]] - [[#notmuch][Notmuch]] - [[#elfeed][Elfeed]] - [[#some-additions][Some additions]] - [[#custom-faces][Custom faces]] - [[#elfeed-score][elfeed-score]] - [[#youtube--emms][YouTube & EMMS]] - [[#emms][EMMS]] - [[#mpd][MPD]] - [[#mpv][MPV]] - [[#cache-cleanup][Cache cleanup]] - [[#fetching-lyrics][Fetching lyrics]] - [[#some-keybindings][Some keybindings]] - [[#emms--mpd-fixes][EMMS & mpd Fixes]] - [[#ytel][ytel]] - [[#eww][EWW]] - [[#erc][ERC]] - [[#google-translate][Google Translate]] - [[#reading-documentation][Reading documentation]] - [[#tldr][tldr]] - [[#man--info][man & info]] - [[#devdocsio][devdocs.io]] - [[#utilities][Utilities]] - [[#pass][pass]] - [[#docker][Docker]] - [[#progidy][Progidy]] - [[#screenshotel][screenshot.el]] - [[#proced][proced]] - [[#guix][Guix]] - [[#productivity][Productivity]] - [[#pomm][pomm]] - [[#guix-settings][Guix settings]] :END: * 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 | |--------------+------------------------------------------| | tab-bar.el | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | spaceline | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 | | code compass | 8594d6f53e42c70bbf903e168607841854818a38 | | vue-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | svelte-mode | 8594d6f53e42c70bbf903e168607841854818a38 | | pomidor | 8594d6f53e42c70bbf903e168607841854818a38 | * Bootstrap Setting up the environment, performance tuning and a few basic settings. ** Packages *** straight.el Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo. The following is a straight.el bootstrap script. References: - [[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 individual package loading time. References: - [[https://github.com/jwiegley/use-package][use-package repo]] #+begin_src emacs-lisp :noweb-ref minimal (straight-use-package 'use-package) (eval-when-compile (require 'use-package)) #+end_src ** Variables & environment This section is about optioning the Emacs config. The following variable is true when my machine is not powerful enough for some resource-heavy packages. #+begin_src emacs-lisp (setq my/lowpower (string= (system-name) "azure")) #+end_src The following is true if Emacs is meant to be used with TRAMP over slow ssh. Take a look at the [[*TRAMP][TRAMP]] section for more details. #+begin_src emacs-lisp (setq my/slow-ssh (or (string= (getenv "IS_TRAMP") "true") (string= (system-name) "dev-digital") (string= (system-name) "violet"))) #+end_src The following is true is Emacs is run on a remote server where I don't need stuff like my org workflow #+begin_src emacs-lisp (setq my/remote-server (or (string= (getenv "IS_REMOTE") "true") (string= (system-name) "dev-digital") (string= (system-name) "violet"))) #+end_src And the following is true if Emacs is run from termux on Android. #+begin_src emacs-lisp (setq my/is-termux (string-match-p (rx (* nonl) "com.termux" (* nonl)) (getenv "HOME"))) #+end_src Also, I sometimes need to know if a program is running inside Emacs (say, inside a terminal emulator). To do that, I set the following environment variable: #+begin_src emacs-lisp (setenv "IS_EMACS" "true") #+end_src Finally, I want to have a minimal Emacs config for debugging purposes. This has just straight.el, use-packages, and evil. #+begin_src emacs-lisp :tangle ~/.emacs.d/init-minimal.el :noweb yes <> #+end_src To launch Emacs with this config, run #+begin_src bash :eval no :tangle no emacs -q -l ~/.emacs.d/init-minimal.el #+end_src ** Performance *** Measure startup speed A small function to print out the loading time and number of GCs during the loading. Can be useful as a point of data for optimizing Emacs startup time. #+begin_src emacs-lisp (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))) #+end_src Set the following to =t= to print debug information during the startup. This will include the order in which the packages are loaded and the loading time of individual packages. #+begin_src emacs-lisp ;; (setq use-package-verbose t) #+end_src *** Garbage collection Just setting ~gc-cons-treshold~ to a larger value. #+begin_src emacs-lisp (setq gc-cons-threshold 80000000) (setq read-process-output-max (* 1024 1024)) #+end_src *** Run garbage collection when Emacs is unfocused Run GC when Emacs loses focus. +Time will tell if that's a good idea.+ Some time has passed, and I still don't know if there is any quantifiable advantage to this, but it doesn't hurt. #+begin_src emacs-lisp (add-hook 'emacs-startup-hook (lambda () (if (boundp 'after-focus-change-function) (add-function :after after-focus-change-function (lambda () (unless (frame-focus-state) (garbage-collect)))) (add-hook 'after-focus-change-function 'garbage-collect)))) #+end_src *** Native compilation Set the number of native compilation jobs to 1 on low-power machines. #+begin_src emacs-lisp (when my/lowpower (setq comp-async-jobs-number 1)) #+end_src ** Anaconda [[https://www.anaconda.com/][Anaconda]] is a free package and environment manager. I currently use it to manage multiple versions of Python and Node.js. Take a look at [[file:Guix.org::*conda][the corresponding entry]] in the Guix config for details about using it on Guix. The following code uses the conda package to activate the base environment on startup if Emacs is launched outside the environment. Also, some strange things are happening if vterm is launched with conda activated from Emacs, so I advise =conda-env-activate= to set an auxiliary environment variable. This variable is used in the [[file:Console.org::*Anaconda][shell config]]. References: - [[https://docs.anaconda.com/][Anaconda docs]] - [[https://github.com/necaris/conda.el][conda.el repo]] #+begin_src emacs-lisp (use-package conda :straight t :if (executable-find "conda") :config (setq conda-anaconda-home (string-replace "/bin/conda" "" (executable-find "conda"))) (setq conda-env-home-directory (expand-file-name "~/.conda/")) (setq conda-env-subdirectory "envs") (advice-add 'conda-env-activate :after (lambda (&rest _) (setenv "EMACS_CONDA_ENV" conda-env-current-name) (setenv "INIT_CONDA" "true")) (advice-add 'conda-env-deactivate :after (lambda (&rest _) (setenv "EMACS_CONDA_ENV" nil) (setenv "INIT_CONDA" nil))) (unless (getenv "CONDA_DEFAULT_ENV") (conda-env-activate "general"))) #+end_src ** Config files *** Custom file location By default, custom writes stuff to =init.el=, which is somewhat annoying. The following makes it write to a separate file =custom.el= #+begin_src emacs-lisp (setq custom-file (concat user-emacs-directory "custom.el")) (load custom-file 'noerror) #+end_src *** authinfo #+begin_src emacs-lisp (setq auth-source-debug nil) #+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 ** Prevent Emacs from closing This adds a confirmation to avoid accidental Emacs closing. #+begin_src emacs-lisp (setq confirm-kill-emacs 'y-or-n-p) #+end_src * General settings ** Keybindings *** general.el general.el provides a convenient interface to manage Emacs keybindings. References: - [[https://github.com/noctuid/general.el][general.el repo]] #+begin_src emacs-lisp (use-package general :straight t :config (general-evil-setup)) #+end_src *** which-key A package that displays the available keybindings in a popup. The package is pretty useful, as Emacs seems to have more keybindings than I can remember at any given point. References: - [[https://github.com/justbur/emacs-which-key][which-key repo]] #+begin_src emacs-lisp (use-package which-key :config (setq which-key-idle-delay (if my/lowpower 1 0.3)) (setq which-key-popup-type 'frame) (which-key-mode) (which-key-setup-side-window-bottom) (set-face-attribute 'which-key-local-map-description-face nil :weight 'bold) :straight t) #+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) (dolist (key (which-key--get-bindings (kbd prefix))) (when level (insert (make-string level ? ))) (insert (apply #'format "%s%s%s\n" key)) (when (string-match-p (rx bos "+" (* nonl)) (substring-no-properties (elt key 2))) (my/dump-bindings-recursive (concat prefix " " (substring-no-properties (car key))) (+ 2 (or level 0)))))) (defun my/dump-bindings (prefix) "Dump keybindings starting with PREFIX in a tree-like form." (interactive "sPrefix: ") (with-current-buffer (get-buffer-create "bindings") (point-max) (erase-buffer) (save-excursion (my/dump-bindings-recursive prefix))) (switch-to-buffer-other-window "bindings")) #+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) (evil-set-undo-system 'undo-tree)) #+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 gives 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 the important 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 the 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 :config (global-evil-matchit-mode 1)) #+end_src **** My additions Do ex search in other buffer. Like =*=, but switch to other buffer and search there. #+begin_src emacs-lisp (defun my/evil-ex-search-word-forward-other-window (count &optional symbol) (interactive (list (prefix-numeric-value current-prefix-arg) evil-symbol-word-search)) (save-excursion (evil-ex-start-word-search nil 'forward count symbol)) (other-window 1) (evil-ex-search-next)) (general-define-key :states '(normal) "&" #'my/evil-ex-search-word-forward-other-window) #+end_src **** evil-collection [[https://github.com/emacs-evil/evil-collection][evil-collection]] is a package that provides evil bindings for a lot of different packages. One can see the complete list in the [[https://github.com/emacs-evil/evil-collection/tree/master/modes][modes]] folder. #+begin_src emacs-lisp :noweb-ref minimal (use-package evil-collection :straight t :after evil :config (evil-collection-init '(eww devdocs proced emms pass calendar dired ivy debug guix calc docker ibuffer geiser pdf info elfeed edebug bookmark company vterm flycheck profiler cider explain-pause-mode notmuch custom xref eshell helpful compile comint git-timemachine magit prodigy slime))) #+end_src *** My keybindings Various keybindings settings that I can't put anywhere else. **** Escape key Use the escape key instead of =C-g= whenever possible. I must have copied it from somewhere, but as I googled to find out the source, I discovered quite a number of variations of the following code over time. I wonder if Richard Dawkins was inspired by something like this a few decades ago. #+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) #+end_src =general.el= has a nice integration with which-key, so I use that to show more descriptive annotations for certain groups of keybindings (the default annotation is just =prefix=). #+begin_src emacs-lisp (my-leader-def "a" '(:which-key "apps")) #+end_src **** Universal argument Change the universal argument to =M-u=. I use =C-u= to scroll up, as I'm used to from vim. #+begin_src emacs-lisp (general-def :keymaps 'universal-argument-map "M-u" 'universal-argument-more) (general-def :keymaps 'override :states '(normal motion emacs insert visual) "M-u" 'universal-argument) #+end_src **** Profiler The built-in profiler is a magnificent tool to troubleshoot performance issues. #+begin_src emacs-lisp (my-leader-def :infix "P" "" '(:which-key "profiler") "s" 'profiler-start "e" 'profiler-stop "p" 'profiler-report) #+end_src **** Buffer switching Some keybindings I used in vim to switch buffers and can't let go of. But I think I started to use these less since I made an attempt in [[*i3 integration][i3 integration]]. #+begin_src emacs-lisp (general-define-key :keymaps 'override "C-" 'evil-window-right "C-" 'evil-window-left "C-" 'evil-window-up "C-" 'evil-window-down "C-h" 'evil-window-left "C-l" 'evil-window-right "C-k" 'evil-window-up "C-j" 'evil-window-down "C-x h" 'previous-buffer "C-x l" 'next-buffer) (general-define-key :keymaps 'evil-window-map "x" 'kill-buffer-and-window "d" 'kill-current-buffer) #+end_src =winner-mode= to keep the history of window states. It doesn't play too well with perspective.el, that is it has a single history list for all of the perspectives. But it is still quite usable. #+begin_src emacs-lisp (winner-mode 1) (general-define-key :keymaps 'evil-window-map "u" 'winner-undo "U" 'winner-redo) #+end_src **** Buffer management #+begin_src emacs-lisp (my-leader-def :infix "b" "" '(:which-key "buffers") "s" '((lambda () (interactive) (switch-to-buffer (persp-scratch-buffer))) :which-key "*scratch*") "m" '((lambda () (interactive) (persp-switch-to-buffer "*Messages*")) :which-key "*Messages*") "l" 'next-buffer "h" 'previous-buffer "k" 'kill-buffer "b" 'persp-ivy-switch-buffer "r" 'revert-buffer "u" 'ibuffer) #+end_src **** xref Some keybindings for xref and go to definition. #+begin_src emacs-lisp (general-nmap "gD" 'xref-find-definitions-other-window "gr" 'xref-find-references "gd" 'evil-goto-definition) (my-leader-def "fx" 'xref-find-apropos) #+end_src **** 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]], but I don't use it at the moment. Evil does a pretty good job of abstracting the first two with a set of vim-like keybindings. I was using =SPC= in vim, but as now this isn't an option, I set =TAB= to toggle folding. #+begin_src emacs-lisp (general-nmap :keymaps '(hs-minor-mode-map outline-minor-mode-map) "ze" 'hs-hide-level "TAB" 'evil-toggle-fold) #+end_src **** Zoom UI #+begin_src emacs-lisp (defun my/zoom-in () "Increase font size by 10 points" (interactive) (set-face-attribute 'default nil :height (+ (face-attribute 'default :height) 10))) (defun my/zoom-out () "Decrease font size by 10 points" (interactive) (set-face-attribute 'default nil :height (- (face-attribute 'default :height) 10))) ;; change font size, interactively (global-set-key (kbd "C-+") 'my/zoom-in) (global-set-key (kbd "C-=") 'my/zoom-out) #+end_src ** i3 integration UPD <2021-11-27 Sat>. I have finally switched to EXWM as my window manager, but as long as I keep i3 as a backup solution, this section persists. Check out the [[https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/][post]] for a somewhat better presentation. One advantage of EXWM for an Emacs user is that EXWM gives one set of keybindings to manage both Emacs windows and X windows. In every other WM, like my preferred [[https://i3wm.org][i3wm]], two orthogonal keymaps seem to be necessary. But, as both programs are quite customizable, I want to see whether I can replicate at least some part of the EXWM goodness in i3. But why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn't feel snappy enough. Also, I really like i3's tree-based layout structure; I feel like it fits my workflow much better than anything else I tried, including the master/stack paradigm of [[https://xmonad.org/][XMonad]]​, for instance. One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs that are configured in an actual programing language, like the mentioned XMonad, [[http://www.qtile.org/][Qtile]], [[https://awesomewm.org/][Awesome]], etc. But I think i3's extensibility is underappreciated, although the contents of this section may lie closer to the limits of how far one can go there. The basic idea is to launch a normal i3 command with =i3-msg= in case the current window is not Emacs, otherwise pass that command to Emacs with =emacsclient=. In Emacs, execute the command if possible, otherwise pass the command back to i3. This may seem like a lot of overhead, but I didn't feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard, the interaction feels seamless. The only concern is that this command flow is vulnerable to Emacs getting stuck, but it is still much less of a problem than with EXWM. One interesting observation here is that Emacs windows and X windows are sort of one-level entities, so I can talk just about "windows". At any rate, we need a script to do the i3 -> Emacs part: #+begin_src bash :tangle ~/bin/scripts/emacs-i3-integration if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then command="(my/emacs-i3-integration \"$@\")" emacsclient -e "$command" else i3-msg $@ fi #+end_src This script is being run from the [[file:Desktop.org::*i3wm][i3 configuration]]. For this to work, we need to make sure that Emacs starts a server, so here is an expression to do just that: #+BEGIN_SRC emacs-lisp (add-hook 'after-init-hook #'server-start) #+END_SRC And here is a simple macro to do the Emacs -> i3 part: #+begin_src emacs-lisp (defmacro i3-msg (&rest args) `(start-process "emacs-i3-windmove" nil "i3-msg" ,@args)) #+end_src Now we have to handle the required set of i3 commands. It is worth noting here that I'm not trying to implement a general mechanism to apply i3 commands to Emacs, rather I'm implementing a small subset that I use in my i3 configuration and that maps reasonably to the Emacs concepts. Also, I use [[https://github.com/emacs-evil/evil][evil-mode]] and generally configure the software to have vim-style bindings where possible. So if you don't use evil-mode you'd have to detangle the given functions from evil, but then, I guess, you do not use super+hjkl to manage windows either. First, for the =focus= command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and windmove have the same names for directions, so the function is rather straightforward. One caveat here is that the minibuffer is always the bottom-most Emacs window, so it is necessary to check for that as well. #+begin_src emacs-lisp (defun my/emacs-i3-windmove (dir) (let ((other-window (windmove-find-other-window dir))) (if (or (null other-window) (window-minibuffer-p other-window)) (i3-msg "focus" (symbol-name dir)) (windmove-do-window-select dir)))) #+end_src For the =move= I want the following behavior: - if there is space in the required direction, move the Emacs window there; - if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions; - otherwise, move an X window (Emacs frame). For the first part, =window-swap-states= with =windmove-find-other-window= do well enough. =evil-move-window= works well for the second part. By itself it doesn't behave quite like i3, for instance, =(evil-move-window 'right)= in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here. So here is a simple predicate which checks whether there is space in the given direction. #+begin_src emacs-lisp (defun my/emacs-i3-direction-exists-p (dir) (cl-some (lambda (dir) (let ((win (windmove-find-other-window dir))) (and win (not (window-minibuffer-p win))))) (pcase dir ('width '(left right)) ('height '(up down))))) #+end_src And the implementation of the move command. #+begin_src emacs-lisp (defun my/emacs-i3-move-window (dir) (let ((other-window (windmove-find-other-window dir)) (other-direction (my/emacs-i3-direction-exists-p (pcase dir ('up 'width) ('down 'width) ('left 'height) ('right 'height))))) (cond ((and other-window (not (window-minibuffer-p other-window))) (window-swap-states (selected-window) other-window)) (other-direction (evil-move-window dir)) (t (i3-msg "move" (symbol-name dir)))))) #+end_src Next on the line are =resize grow= and =resize shrink=. =evil-window-= functions do nicely for this task. This function also checks whether there is space to resize in the given direction with the help of the predicate defined above. The command is forwarded back to i3 if there is not. #+begin_src emacs-lisp (defun my/emacs-i3-resize-window (dir kind value) (if (or (one-window-p) (not (my/emacs-i3-direction-exists-p dir))) (i3-msg "resize" (symbol-name kind) (symbol-name dir) (format "%s px or %s ppt" value value)) (setq value (/ value 2)) (pcase kind ('shrink (pcase dir ('width (evil-window-decrease-width value)) ('height (evil-window-decrease-height value)))) ('grow (pcase dir ('width (evil-window-increase-width value)) ('height (evil-window-increase-height value))))))) #+end_src [[https://github.com/emacsorphanage/transpose-frame][transpose-frame]] is a package to "transpose" the current frame layout, which behaves someone similar to the =layout toggle split= command in i3, so I'll use it as well. #+begin_src emacs-lisp (use-package transpose-frame :straight t :commands (transpose-frame)) #+end_src Finally, the entrypoint for the Emacs integration. In addition to the commands defined above, it processes =split= and =kill= commands and passes every other command back to i3. #+begin_src emacs-lisp (defun my/emacs-i3-integration (command) (pcase command ((rx bos "focus") (my/emacs-i3-windmove (intern (elt (split-string command) 1)))) ((rx bos "move") (my/emacs-i3-move-window (intern (elt (split-string command) 1)))) ((rx bos "resize") (my/emacs-i3-resize-window (intern (elt (split-string command) 2)) (intern (elt (split-string command) 1)) (string-to-number (elt (split-string command) 3)))) ("layout toggle split" (transpose-frame)) ("split h" (evil-window-split)) ("split v" (evil-window-vsplit)) ("kill" (evil-quit)) (- (i3-msg command)))) #+end_src ** Editing text Various packages, tricks, and settings that help with the central task of Emacs - editing text. *** Indentation & whitespace **** Aggressive Indent A package to keep the code intended. Doesn't work too well with many ecosystems because the LSP-based indentation is rather slow but nice for Lisps. References: - [[https://github.com/Malabarba/aggressive-indent-mode][aggressive-indent-mode repo]] #+begin_src emacs-lisp (use-package aggressive-indent :commands (aggressive-indent-mode) :straight t) #+end_src **** Delete trailing whitespace Delete trailing whitespace on save, unless in particular modes where trailing whitespace is important, like Markdown. #+begin_src emacs-lisp (setq my/trailing-whitespace-modes '(markdown-mode)) (require 'cl-extra) (add-hook 'before-save-hook (lambda () (unless (cl-some #'derived-mode-p my/trailing-whitespace-modes) (delete-trailing-whitespace)))) #+end_src **** Tabs Some default settings to manage tabs. #+begin_src emacs-lisp (setq tab-always-indent nil) (setq-default default-tab-width 4) (setq-default tab-width 4) (setq-default evil-indent-convert-tabs nil) (setq-default indent-tabs-mode nil) (setq-default tab-width 4) (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) (setq undo-tree-visualizer-diff t) (setq undo-tree-visualizer-timestamps t) (my-leader-def "u" 'undo-tree-visualize) (fset 'undo-auto-amalgamate 'ignore) (setq undo-limit 6710886400) (setq undo-strong-limit 100663296) (setq undo-outer-limit 1006632960)) #+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. References: - [[http://joaotavora.github.io/yasnippet/][yasnippet documentation]] #+begin_src emacs-lisp (use-package yasnippet-snippets :straight t) (use-package yasnippet :straight t :config (setq yas-snippet-dirs `(,(concat (expand-file-name user-emacs-directory) "snippets") yasnippet-snippets-dir)) (setq yas-triggers-in-field t) (yas-global-mode 1)) (general-imap "M-TAB" 'company-yasnippet) #+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 **** Expand region A package to select an ever-increasing (or ever-decreasing) region of text. #+begin_src emacs-lisp (use-package expand-region :straight t :commands (er/expand-region) :init (general-nmap "+" 'er/expand-region)) #+end_src **** Visual fill column mode #+begin_src emacs-lisp (use-package visual-fill-column :straight t :commands (visual-fill-column-mode) :config (add-hook 'visual-fill-column-mode-hook (lambda () (setq visual-fill-column-center-text t)))) #+end_src ** Working with projects *** Treemacs [[https://github.com/Alexander-Miller/treemacs][Treemacs]] calls itself a tree layout file explorer, but looks more like a project and workspace management system. Integrates with evil, magit, projectile, and perspective. The latter is particularly great - each perspective can have its own treemacs workspace! #+begin_src emacs-lisp (use-package treemacs :straight t :commands (treemacs treemacs-switch-workspace treemacs-edit-workspace) :config (setq treemacs-follow-mode nil) (setq treemacs-follow-after-init nil) (setq treemacs-space-between-root-nodes nil) (treemacs-git-mode 'extended) (add-to-list 'treemacs-pre-file-insert-predicates #'treemacs-is-file-git-ignored?) (general-define-key :keymaps 'treemacs-mode-map [mouse-1] #'treemacs-single-click-expand-action "M-l" #'treemacs-root-down "M-h" #'treemacs-root-up)) (use-package treemacs-evil :after (treemacs evil) :straight t) (use-package treemacs-magit :after (treemacs magit) :straight t) (use-package treemacs-perspective :after (treemacs perspective) :straight t :config (treemacs-set-scope-type 'Perspectives)) (general-define-key :keymaps '(normal override global) "C-n" 'treemacs) (general-define-key :keymaps '(treemacs-mode-map) [mouse-1] #'treemacs-single-click-expand-action) (my-leader-def :infix "t" "" '(:which-key "treemacs") "w" 'treemacs-switch-workspace "e" 'treemacs-edit-workspaces) #+end_src **** Helper functions Function to open dired and vterm at given nodes. #+begin_src emacs-lisp (defun my/treemacs-open-dired () "Open dired at given treemacs node" (interactive) (let (path (treemacs--prop-at-point :path)) (dired path))) (defun my/treemacs-open-vterm () "Open vterm at given treemacs node" (interactive) (let ((default-directory (file-name-directory (treemacs--prop-at-point :path)))) (vterm))) (with-eval-after-load 'treemacs (general-define-key :keymaps 'treemacs-mode-map :states '(treemacs) "gd" 'my/treemacs-open-dired "gt" 'my/treemacs-open-vterm "`" 'my/treemacs-open-vterm)) #+end_src **** Custom icons #+begin_src emacs-lisp ;; (treemacs-define-custom-icon (concat " " (all-the-icons-fileicon "typescript")) "spec.ts") ;; (setq treemacs-file-extension-regex (rx "." (or "spec.ts" (+ (not "."))) eos)) #+end_src *** Projectile [[https://github.com/bbatsov/projectile][Projectile]] gives a bunch of useful functions for managing projects, like finding files within a project, fuzzy-find, replace, etc. ~defadvice~ is meant to speed projectile up with TRAMP a bit. #+begin_src emacs-lisp (use-package projectile :straight t :config (projectile-mode +1) (setq projectile-project-search-path '("~/Code" "~/Documents")) (defadvice projectile-project-root (around ignore-remote first activate) (unless (file-remote-p default-directory) ad-do-it))) (use-package counsel-projectile :after (counsel projectile) :straight t) (use-package treemacs-projectile :after (treemacs projectile) :straight t) (my-leader-def "p" '(:keymap projectile-command-map :which-key "projectile")) (general-nmap "C-p" 'counsel-projectile-find-file) #+end_src *** Git & Magit [[https://magit.vc/][Magit]] is a git interface for Emacs. The closest non-Emacs alternative (sans actual clones) I know is [[https://github.com/jesseduffield/lazygit][lazygit]], which I used before Emacs. [[https://github.com/magit/forge][forge]] provides integration with forges, such as GitHub and GitLab. [[https://github.com/emacsorphanage/git-gutter][git-gutter]] is a package which shows git changes for each line (added/changed/deleted lines). [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] allows visiting previous versions of a file. #+begin_src emacs-lisp (use-package magit :straight t :commands (magit-status magit-file-dispatch) :config (setq magit-blame-styles '((margin (margin-format . ("%a %A %s")) (margin-width . 42) (margin-face . magit-blame-margin) (margin-body-face . (magit-blame-dimmed))) (headings (heading-format . "%-20a %C %s\n")) (highlight (highlight-face . magit-blame-highlight)) (lines (show-lines . t) (show-message . t))))) (use-package forge :after magit :straight t :config (add-to-list 'forge-alist '("gitlab.etu.ru" "gitlab.etu.ru/api/v4" "gitlab.etu.ru" forge-gitlab-repository))) (use-package git-gutter :straight t :if (not my/slow-ssh) :config (global-git-gutter-mode +1)) (use-package git-timemachine :straight t :commands (git-timemachine)) (my-leader-def "m" 'magit "M" 'magit-file-dispatch) #+end_src *** Editorconfig Editorconfig support for Emacs. References: - [[https://editorconfig.org/][Editorconfig reference]] #+begin_src emacs-lisp (use-package editorconfig :straight t :config (unless my/slow-ssh (editorconfig-mode 1)) (add-to-list 'editorconfig-indentation-alist '(emmet-mode emmet-indentation))) #+end_src ** Completion *** Ivy, counsel, swiper Minibuffer completion tools for Emacs. References: - [[https://oremacs.com/swiper/][repo]] - [[https://oremacs.com/swiper/][User Manual]] #+begin_src emacs-lisp (use-package ivy :straight t :config (setq ivy-use-virtual-buffers t) (ivy-mode)) (use-package counsel :straight t :after ivy :config (counsel-mode)) (use-package swiper :defer t :straight t) #+end_src *** ivy-rich [[https://github.com/Yevgnen/ivy-rich][ivy-rich]] provides a more informative interface for ivy. #+begin_src emacs-lisp (use-package ivy-rich :straight t :after ivy :config (ivy-rich-mode 1) (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line)) #+end_src *** prescient A package that enhances sorting & filtering of candidates. =ivy-prescient= adds integration with Ivy. References: - [[https://github.com/raxod502/prescient.el][prescient.el repo]] #+begin_src emacs-lisp :noweb yes (use-package ivy-prescient :straight t :after counsel :config (ivy-prescient-mode +1) (setq ivy-prescient-retain-classic-highlighting t) (prescient-persist-mode 1) (setq ivy-prescient-sort-commands '(:not swiper swiper-isearch ivy-switch-buffer ;; ivy-resume ;; ivy--restore-session lsp-ivy-workspace-symbol dap-switch-stack-frame my/dap-switch-stack-frame dap-switch-session dap-switch-thread counsel-grep ;; counsel-find-file counsel-git-grep counsel-rg counsel-ag counsel-ack counsel-fzf counsel-pt counsel-imenu counsel-yank-pop counsel-recentf counsel-buffer-or-recentf proced-filter-interactive proced-sort-interactive perspective-exwm-switch-perspective my/persp-ivy-switch-buffer-other-window lsp-execute-code-action)) ;; Do not use prescient in find-file (ivy--alist-set 'ivy-sort-functions-alist #'read-file-name-internal #'ivy-sort-file-function-default)) #+end_src *** keybindings Setting up quick access to various completions. #+begin_src emacs-lisp (my-leader-def :infix "f" "" '(:which-key "various completions")' ;; "b" 'counsel-switch-buffer "b" 'persp-ivy-switch-buffer "e" 'conda-env-activate "f" 'project-find-file "c" 'counsel-yank-pop "a" 'counsel-rg "A" 'counsel-ag) (general-define-key :states '(insert normal) "C-y" 'counsel-yank-pop) (my-leader-def "SPC" 'ivy-resume) (my-leader-def "s" 'swiper-isearch "S" 'swiper-all) (general-define-key :keymaps '(ivy-minibuffer-map swiper-map) "M-j" 'ivy-next-line "M-k" 'ivy-previous-line "" 'ivy-call "M-RET" 'ivy-immediate-done [escape] 'minibuffer-keyboard-quit) #+end_src *** company A completion framework for Emacs. References: - [[http://company-mode.github.io/][company homepage]] - [[https://github.com/sebastiencs/company-box][company-box homepage]] #+begin_src emacs-lisp (use-package company :straight t :config (global-company-mode) (setq company-idle-delay (if my/lowpower 0.5 0.125)) (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. #+begin_src emacs-lisp (use-package company-box :straight t :if (and (display-graphic-p) (not my/lowpower)) :after (company) :hook (company-mode . company-box-mode)) #+end_src ** Help - *CREDIT*: Thanks @phundrak on the System Crafters Discord for suggesting =help-map= [[https://github.com/Wilfred/helpful][helpful]] package improves the =*help*= buffer. #+begin_src emacs-lisp (use-package helpful :straight t :commands (helpful-callable helpful-variable helpful-key helpful-macro helpful-function helpful-command)) #+end_src As I use =C-h= to switch buffers, I moved the help to =SPC-h= with the code below. #+begin_src emacs-lisp (my-leader-def "h" '(:keymap help-map :which-key "help")) (general-define-key :keymaps 'help-map "f" 'helpful-function "k" 'helpful-key "v" 'helpful-variable "o" 'helpful-symbol) #+end_src ** Time trackers A bunch of time trackers I use. References: - [[https://wakatime.com][WakaTime]] - [[https://activitywatch.net/][ActivityWatch]] *** WakaTime Before I figure out how to package this for Guix: - Clone [[https://github.com/wakatime/wakatime-cli][the repo]] - Run ~go build~ - Copy the binary to the =~/bin= folder #+begin_src emacs-lisp :noweb yes (use-package wakatime-mode :straight (:host github :repo "SqrtMinusOne/wakatime-mode") :if (not (or my/is-termux my/remote-server)) :config (setq wakatime-ignore-exit-codes '(0 1 102)) (advice-add 'wakatime-init :after (lambda () (setq wakatime-cli-path "/home/pavel/bin/wakatime-cli"))) ;; (setq wakatime-cli-path (executable-find "wakatime")) (global-wakatime-mode)) #+end_src *** ActivityWatch #+begin_src emacs-lisp (use-package request :straight t) (use-package activity-watch-mode :straight t :if (not (or my/is-termux my/remote-server)) :config (global-activity-watch-mode)) #+end_src * UI settings ** General settings *** Miscellaneous Disable GUI elements #+begin_src emacs-lisp (unless my/is-termux (tool-bar-mode -1) (menu-bar-mode -1) (scroll-bar-mode -1)) #+end_src Transparency. Not setting it now, as I'm using [[file:Desktop.org::*Picom][picom]]. #+begin_src emacs-lisp ;; (set-frame-parameter (selected-frame) 'alpha '(90 . 90)) ;; (add-to-list 'default-frame-alist '(alpha . (90 . 90))) #+end_src Prettify symbols. Also not setting it, ligatures seem to be enough for me. #+begin_src emacs-lisp ;; (global-prettify-symbols-mode) #+end_src 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 (makes 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= truncate long lines instted 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 looks something like =emacs:project@hostname=. #+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 ** Themes and colors *** 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)) #+end_src *** Doom themes My colorscheme of choice. #+begin_src emacs-lisp (use-package doom-themes :straight t :if (not my/is-termux) :config (setq doom-themes-enable-bold t doom-themes-enable-italic t) (if my/remote-server (load-theme 'doom-gruvbox t) (load-theme 'doom-palenight t)) (doom-themes-visual-bell-config) (setq doom-themes-treemacs-theme "doom-colors") (doom-themes-treemacs-config)) #+end_src *** Custom theme A custom theme, dependent on Doom. I set all my custom variables there. A custom theme is necessary because if one calls =custom-set-faces= and =custom-set-variables= in code, whenever a variable is changed and saved in a customize buffer, data from all calls of these functions is saved as well. Also, a hook allows me to change doom-theme more or less at will, although I do that only to switch to a light theme once in a blue moon. #+begin_src emacs-lisp (unless my/is-termux (deftheme my-theme-1) (defun my/update-my-theme (&rest _) (custom-theme-set-faces 'my-theme-1 `(tab-bar-tab ((t ( :background ,(doom-color 'bg) :foreground ,(doom-color 'yellow) :underline ,(doom-color 'yellow))))) `(tab-bar ((t (:background nil :foreground nil)))) `(org-block ((t (:background ,(color-darken-name (doom-color 'bg) 3))))) `(org-block-begin-line ((t ( :background ,(color-darken-name (doom-color 'bg) 3) :foreground ,(doom-color 'grey))))) `(auto-dim-other-buffers-face ((t (:background ,(color-darken-name (doom-color 'bg) 3))))) `(aweshell-alert-buffer-face ((t (:foreground ,(doom-color 'red) :weight bold)))) `(aweshell-alert-command-face ((t (:foreground ,(doom-color 'yellow) :weight bold)))) `(epe-pipeline-delimiter-face ((t (:foreground ,(doom-color 'green))))) `(epe-pipeline-host-face ((t (:foreground ,(doom-color 'blue))))) `(epe-pipeline-time-face ((t (:foreground ,(doom-color 'yellow))))) `(epe-pipeline-user-face ((t (:foreground ,(doom-color 'red))))) `(elfeed-search-tag-face ((t (:foreground ,(doom-color 'yellow))))) `(notmuch-wash-cited-text ((t (:foreground ,(doom-color 'yellow))))) `(spaceline-evil-emacs ((t :background ,(doom-color 'bg) :foreground ,(doom-color 'fg)))) `(spaceline-evil-insert ((t :background ,(doom-color 'green) :foreground ,(doom-color 'base0)))) `(spaceline-evil-motion ((t :background ,(doom-color 'magenta) :foreground ,(doom-color 'base0)))) `(spaceline-evil-normal ((t :background ,(doom-color 'blue) :foreground ,(doom-color 'base0)))) `(spaceline-evil-replace ((t :background ,(doom-color 'yellow) :foreground ,(doom-color 'base0)))) `(spaceline-evil-visual ((t :background ,(doom-color 'grey) :foreground ,(doom-color 'base0))))) (custom-theme-set-variables 'my-theme-1 `(aweshell-invalid-command-color ,(doom-color 'red)) `(aweshell-valid-command-color ,(doom-color 'green))) (enable-theme 'my-theme-1)) (advice-add 'load-theme :after #'my/update-my-theme) (when (fboundp 'doom-color) (my/update-my-theme))) #+end_src ** Fonts *** Frame font To install a font, download the font and unpack it into the =.local/share/fonts= directory. Create one if it doesn't exist. As I use nerd fonts elsewhere, I use one in Emacs as well. References: - [[https://nerdfonts.com][nerd fonts homepage]] #+begin_src emacs-lisp (set-frame-font "JetBrainsMono Nerd Font 10" nil t) #+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. *** 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 js2-mode vue-mode svelte-mode scss-mode php-mode python-mode js-mode markdown-mode clojure-mode go-mode sh-mode haskell-mode web-mode) '("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<>" "<<<" ">>>" "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "" "/>" "" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" ">--" "-<" "-<<" ">->" "<-<" "<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".." ".=" ".-" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__")) (global-ligature-mode t)) #+end_src *** Icons #+begin_src emacs-lisp (use-package all-the-icons :if (display-graphic-p) :straight t) #+end_src ** Text highlight Highlight indent guides. #+begin_src emacs-lisp (use-package highlight-indent-guides :straight t :if (not (or my/lowpower my/remote-server)) :hook ( (prog-mode . highlight-indent-guides-mode) (vue-mode . highlight-indent-guides-mode) (LaTeX-mode . highlight-indent-guides-mode)) :config (setq highlight-indent-guides-method 'bitmap) (setq highlight-indent-guides-bitmap-function 'highlight-indent-guides--bitmap-line)) #+end_src Rainbow parentheses. #+begin_src emacs-lisp (use-package rainbow-delimiters :straight t :if (not my/lowpower) :hook ((prog-mode . rainbow-delimiters-mode))) #+end_src Highlight colors #+begin_src emacs-lisp (use-package rainbow-mode :commands (rainbow-mode) :straight t) #+end_src Highlight TODOs and stuff #+begin_src emacs-lisp (use-package hl-todo :hook (prog-mode . hl-todo-mode) :straight t) #+end_src ** Doom Modeline A modeline from Doom Emacs. A big advantage of this package is that it just works out of the box and does not require much customization. I tried a bunch of other options, including [[https://github.com/TheBB/spaceline][spaceline]], but in the end, decided that Doom Modeline works best for me. References: - [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] #+begin_src emacs-lisp (use-package doom-modeline :straight t ;; :if (not (display-graphic-p)) :init (setq doom-modeline-env-enable-python nil) (setq doom-modeline-env-enable-go nil) (setq doom-modeline-buffer-encoding 'nondefault) (setq doom-modeline-hud t) (setq doom-modeline-persp-icon nil) (setq doom-modeline-persp-name nil) :config (setq doom-modeline-minor-modes nil) (setq doom-modeline-buffer-state-icon nil) (doom-modeline-mode 1)) #+end_src ** perspective.el [[https://github.com/nex3/perspective-el][perspective.el]] is a package which provides gives Emacs capacities to group buffers into "perspectives", which are like workspaces in tiling WMs. An advantage over =tab-bar.el= is that =perspective.el= has better capacities for managing buffers, e.g. gives an ibuffer-like interface inside a perspective. However, I don't like that list of workspaces is displayed inside the modeline rather than in an actual bar on the top of the frame. I may look into that later. #+begin_src emacs-lisp (use-package perspective :straight t :init ;; (setq persp-show-modestring 'header) (setq persp-sort 'created) :config (persp-mode) (my-leader-def "x" '(:keymap perspective-map :which-key "perspective")) (general-define-key :keymaps 'override :states '(normal emacs) "gt" 'persp-next "gT" 'persp-prev "gn" 'persp-switch "gN" 'persp-kill) (general-define-key :keymaps 'perspective-map "b" 'persp-ivy-switch-buffer "x" 'persp-ivy-switch-buffer "u" 'persp-ibuffer)) #+end_src *** Functions to manage buffers Move the current buffer to a perspective and switch to it. #+begin_src emacs-lisp (defun my/persp-move-window-and-switch () (interactive) (let* ((buffer (current-buffer))) (call-interactively #'persp-switch) (persp-set-buffer (buffer-name buffer)) (switch-to-buffer buffer))) #+end_src Copy the current buffer to a perspective and switch to it. #+begin_src emacs-lisp (defun my/persp-copy-window-and-switch () (interactive) (let* ((buffer (current-buffer))) (call-interactively #'persp-switch) (persp-add-buffer (buffer-name buffer)) (switch-to-buffer buffer))) #+end_src Switch to a perspective buffer in other window. #+begin_src emacs-lisp (defun my/persp-ivy-switch-buffer-other-window (arg) (interactive "P") (declare-function ivy-switch-buffer-other-window "ivy.el") (persp--switch-buffer-ivy-counsel-helper arg (lambda () (ivy-read "Switch to buffer in other window: " #'internal-complete-buffer :keymap ivy-switch-buffer-map :preselect (buffer-name (other-buffer (current-buffer))) :action #'ivy--switch-buffer-other-window-action :matcher #'ivy--switch-buffer-matcher :caller 'ivy-switch-buffer)))) #+end_src Add keybindings to the default map. #+begin_src emacs-lisp (with-eval-after-load 'perspective (general-define-key :keymaps 'perspective-map "m" #'my/persp-move-window-and-switch "f" #'my/persp-copy-window-and-switch)) #+end_src *** Automating perspectives I'd like to have various Emacs apps open up in their designated perspectives (also in their designated workspaces when I'm using EXWM). So, here is a macro to run something in a given perspective in a given workspace. This is meant to be used in general.el keybindings. #+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 * Programming ** General setup *** LSP LSP-mode provides an IDE-like experience for Emacs - real-time diagnostic, code actions, intelligent autocompletion, etc. References: - [[https://emacs-lsp.github.io/lsp-mode/][lsp-mode homepage]] **** Setup #+begin_src emacs-lisp (use-package lsp-mode :straight t :if (not (or my/slow-ssh my/is-termux my/remote-server)) :hook ( (typescript-mode . lsp) (js-mode . lsp) (vue-mode . lsp) (go-mode . lsp) (svelte-mode . lsp) ;; (python-mode . lsp) (json-mode . lsp) (haskell-mode . lsp) (haskell-literate-mode . lsp) (java-mode . lsp) ;; (csharp-mode . lsp) ) :commands lsp :init (setq lsp-keymap-prefix nil) :config (setq lsp-idle-delay 1) (setq lsp-eslint-server-command '("node" "/home/pavel/.emacs.d/.cache/lsp/eslint/unzipped/extension/server/out/eslintServer.js" "--stdio")) (setq lsp-eslint-run "onSave") (setq lsp-signature-render-documentation nil) ;; (lsp-headerline-breadcrumb-mode nil) (setq lsp-headerline-breadcrumb-enable nil) (setq lsp-modeline-code-actions-enable nil) (setq lsp-modeline-diagnostics-enable nil) (add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte"))) (use-package lsp-ui :straight t :commands lsp-ui-mode :config (setq lsp-ui-doc-delay 2) (setq lsp-ui-sideline-show-hover nil)) #+end_src **** Integrations The only integration left now is treemacs. Origami should've leveraged LSP folding, but it was too unstable at the moment I tried it. #+begin_src emacs-lisp ;; (use-package helm-lsp ;; :straight t ;; :commands helm-lsp-workspace-symbol) ;; (use-package origami ;; :straight t ;; :hook (prog-mode . origami-mode)) ;; (use-package lsp-origami ;; :straight t ;; :config ;; (add-hook 'lsp-after-open-hook #'lsp-origami-try-enable)) (use-package lsp-treemacs :after (lsp) :straight t :commands lsp-treemacs-errors-list) #+end_src **** Keybindings #+begin_src emacs-lisp (my-leader-def :infix "l" "" '(:which-key "lsp") "d" 'lsp-ui-peek-find-definitions "r" 'lsp-rename "u" 'lsp-ui-peek-find-references "s" 'lsp-ui-find-workspace-symbol "l" 'lsp-execute-code-action "e" 'list-flycheck-errors) #+end_src *** Flycheck A syntax checking extension for Emacs. Integrates with LSP-mode, but can also use various standalone checkers. References: - [[https://www.flycheck.org/en/latest/][Flycheck homepage]] #+begin_src emacs-lisp (use-package flycheck :straight t :config (global-flycheck-mode) (setq flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled)) ;; (add-hook 'evil-insert-state-exit-hook ;; (lambda () ;; (if flycheck-checker ;; (flycheck-buffer)) ;; )) (advice-add 'flycheck-eslint-config-exists-p :override (lambda() t)) (add-to-list 'display-buffer-alist `(,(rx bos "*Flycheck errors*" eos) (display-buffer-reuse-window display-buffer-in-side-window) (side . bottom) (reusable-frames . visible) (window-height . 0.33)))) #+end_src *** Tree Sitter An incremental code parsing system, constructing a syntax tree at runtime. Right now it doesn't do much except provide a better syntax highlighting than regexes, but this integration is a rather recent development. There are already some major modes built on top of this thing. Also, it seems to break if run from mmm-mode, so there is a small workaround. References: - [[https://tree-sitter.github.io/tree-sitter/][Tree-sitter library]] - [[https://ubolonton.github.io/emacs-tree-sitter/][Emacs Tree-sitter]] #+begin_src emacs-lisp (defun my/tree-sitter-if-not-mmm () (when (not (and (boundp 'mmm-temp-buffer-name) (string-equal mmm-temp-buffer-name (buffer-name)))) (tree-sitter-mode) (tree-sitter-hl-mode))) (use-package tree-sitter :straight t :if (not my/remote-server) :hook ((typescript-mode . my/tree-sitter-if-not-mmm) (js-mode . my/tree-sitter-if-not-mmm) (python-mode . tree-sitter-mode) (python-mode . tree-sitter-hl-mode) (csharp-mode . tree-sitter-mode))) (use-package tree-sitter-langs :straight t :after tree-sitter) #+end_src *** DAP An Emacs client for Debugger Adapter Protocol. As of the time of this writing, I mostly debug TypeScript, so the main competitor is Chrome Inspector for node.js. References: - [[https://emacs-lsp.github.io/dap-mode/][dap-mode homepage]] #+begin_src emacs-lisp (use-package dap-mode :straight t :commands (dap-debug) :init (setq lsp-enable-dap-auto-configure nil) :config (setq dap-ui-variable-length 100) (setq dap-auto-show-output nil) (require 'dap-node) (dap-node-setup) (require 'dap-chrome) (dap-chrome-setup) (require 'dap-python) (dap-mode 1) (dap-ui-mode 1) (dap-tooltip-mode 1) (tooltip-mode 1)) #+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 **** Improved stack frame switching One significant improvement over Chrome Inspector for my particular stack is an ability to filter the stack frame list, for instance, to see only frames that relate to my current project. So, here are functions that customize the filters: #+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: #+begin_src emacs-lisp (defun my/dap--go-to-stack-frame-override (debug-session stack-frame) "Make STACK-FRAME the active STACK-FRAME of DEBUG-SESSION." (with-lsp-workspace (dap--debug-session-workspace debug-session) (when stack-frame (-let* (((&hash "line" line "column" column "name" name) stack-frame) (path (dap--get-path-for-frame stack-frame))) (setf (dap--debug-session-active-frame debug-session) stack-frame) ;; If we have a source file with path attached, open it and ;; position the point in the line/column referenced in the ;; stack trace. (if (and path (file-exists-p path)) (progn (let ((exwm-target (my/exwm-perspective-find-buffer path))) (if exwm-target (progn (unless (= (cdr exwm-target) exwm-workspace-current-index) (exwm-workspace-switch (cdr exwm-target))) (persp-switch-to-buffer (car exwm-target))) (select-window (get-mru-window (selected-frame) nil)) (find-file path))) (goto-char (point-min)) (forward-line (1- line)) (forward-char column)) (message "No source code for %s. Cursor at %s:%s." name line column)))) (run-hook-with-args 'dap-stack-frame-changed-hook debug-session))) (with-eval-after-load 'exwm (with-eval-after-load 'dap-mode (advice-add #'dap--go-to-stack-frame :override #'my/dap--go-to-stack-frame-override))) ;; (advice-remove #'dap--go-to-stack-frame #'my/dap--go-to-stack-frame-override) #+end_src **** Debug templates Some debug templates I frequently use. #+begin_src emacs-lisp (with-eval-after-load 'dap-mode (dap-register-debug-template "Node::Nest.js" (list :type "node" :request "attach" :name "Node::Attach" :port 9229 :outFiles ["${workspaceFolder}/dist/**/*.js"] :sourceMaps t :program "${workspaceFolder}/src/app.ts")) (dap-register-debug-template "Node::Babel" (list :type "node" :request "attach" :name "Node::Attach" :port 9229 :program "${workspaceFolder}/dist/bin/www.js"))) #+end_src *** Reformatter A general-purpose package to run formatters on files. While the most popular formatters are already packaged for Emacs, those that aren't can be invoked with this package. #+begin_src emacs-lisp (use-package reformatter :straight t) #+end_src *** General additional config Make smartparens behave the way I like for C-like languages. #+begin_src emacs-lisp (defun my/set-smartparens-indent (mode) (sp-local-pair mode "{" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET"))) (sp-local-pair mode "[" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET"))) (sp-local-pair mode "(" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET")))) #+end_src Override flycheck checker with eslint. #+begin_src emacs-lisp (defun my/set-flycheck-eslint() "Override flycheck checker with eslint." (setq-local lsp-diagnostic-package :none) (setq-local flycheck-checker 'javascript-eslint)) #+end_src ** Web development Configs for various web development technologies I'm using. *** Emmet [[https://emmet.io/][Emmet]] is a toolkit which greatly speeds up typing HTML & CSS. | Type | Note | |------+---------------------------------------------------| | TODO | make expand div[disabled] as
| My bit of config here: - makes Emmet activate only in certain mmm-mode submodes. - makes =TAB= the only key I have to use #+begin_src emacs-lisp (use-package emmet-mode :straight t :hook ((vue-html-mode . emmet-mode) (svelte-mode . emmet-mode) (web-mode . emmet-mode) (html-mode . emmet-mode) (css-mode . emmet-mode) (scss-mode . emmet-mode)) :config ;; (setq emmet-indent-after-insert nil) (setq my/emmet-mmm-submodes '(vue-html-mode css-mode)) (defun my/emmet-or-tab (&optional arg) (interactive) (if (and (boundp 'mmm-current-submode) mmm-current-submode (not (member mmm-current-submode my/emmet-mmm-submodes))) (indent-for-tab-command arg) (or (emmet-expand-line arg) (emmet-go-to-edit-point 1) (indent-for-tab-command arg)))) (general-imap :keymaps 'emmet-mode-keymap "TAB" 'my/emmet-or-tab "" '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 vue-mode-map svelte-mode-map) "rr" #'prettier-prettify)) #+end_src *** TypeScript #+begin_src emacs-lisp (use-package typescript-mode :straight t :mode "\\.ts\\'" :config (add-hook 'typescript-mode-hook #'smartparens-mode) (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode) (add-hook 'typescript-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'typescript-mode)) #+end_src *** JavaScript #+begin_src emacs-lisp (add-hook 'js-mode-hook #'smartparens-mode) (add-hook 'js-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'js-mode) #+end_src *** Jest #+begin_src emacs-lisp (use-package jest-test-mode :straight t :hook ((typescript-mode . jest-test-mode) (js-mode . jest-test-mode)) :config (my-leader-def :keymaps 'jest-test-mode-map :infix "t" "t" 'jest-test-run-at-point "r" 'jest-test-run "a" 'jest-test-run-all-tests)) #+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-example-at-point))) (if (and filename example) (jest-test-from-project-directory filename (let ((jest-test-options (seq-concatenate 'list jest-test-options (list "-t" example)))) (kill-new (jest-test-command filename)))) (message jest-test-not-found-message)))) #+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. #+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)) #+end_src Hooking this up with lsp. #+begin_src emacs-lisp (setq my/web-mode-lsp-extensions `(,(rx ".svelte" eos) ,(rx ".vue" eos))) (defun my/web-mode-lsp () (when (seq-some (lambda (regex) (string-match-p regex (buffer-name))) my/web-mode-lsp-extensions) (lsp-deferred))) (add-hook 'web-mode-hook #'my/web-mode-lsp) #+end_src Vue settings #+begin_src emacs-lisp (defun my/web-mode-vue-setup () (when (string-match-p (rx ".vue" eos) (buffer-name)) (setq-local web-mode-script-padding 0))) (add-hook 'web-mode-hook 'my/web-mode-vue-setup) #+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\\'") #+end_src ** LaTeX *** AUCTeX The best LaTeX editing environment I've found so far. References: - [[https://www.gnu.org/software/auctex/][AUCTeX homepage]] #+begin_src emacs-lisp :noweb yes (use-package tex :straight auctex :defer t :config (setq-default TeX-auto-save t) (setq-default TeX-parse-self t) (TeX-PDF-mode) ;; Use XeLaTeX & stuff (setq-default TeX-engine 'xetex) (setq-default TeX-command-extra-options "-shell-escape") (setq-default TeX-source-correlate-method 'synctex) (TeX-source-correlate-mode) (setq-default TeX-source-correlate-start-server t) (setq-default LaTeX-math-menu-unicode t) (setq-default font-latex-fontify-sectioning 1.3) ;; Scale preview for my DPI (setq-default preview-scale-function 1.4) (when (boundp 'tex--prettify-symbols-alist) (assoc-delete-all "--" tex--prettify-symbols-alist) (assoc-delete-all "---" tex--prettify-symbols-alist)) (add-hook 'LaTeX-mode-hook (lambda () (TeX-fold-mode 1) (outline-minor-mode))) (add-to-list 'TeX-view-program-selection '(output-pdf "Zathura")) ;; Do not run lsp within templated TeX files (add-hook 'LaTeX-mode-hook (lambda () (unless (string-match "\.hogan\.tex$" (buffer-name)) (lsp)) (setq-local lsp-diagnostic-package :none) (setq-local flycheck-checker 'tex-chktex))) (add-hook 'LaTeX-mode-hook #'rainbow-delimiters-mode) (add-hook 'LaTeX-mode-hook #'smartparens-mode) (add-hook 'LaTeX-mode-hook #'prettify-symbols-mode) (my/set-smartparens-indent 'LaTeX-mode) (require 'smartparens-latex) (general-nmap :keymaps '(LaTeX-mode-map latex-mode-map) "RET" 'TeX-command-run-all "C-c t" 'orgtbl-mode) <> <> <> <>) #+end_src *** BibTeX #+begin_src emacs-lisp (use-package ivy-bibtex :commands (ivy-bibtex) :straight t :init (my-leader-def "fB" 'ivy-bibtex)) (add-hook 'bibtex-mode 'smartparens-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 ** Other markup & natural languages *** Markdown #+begin_src emacs-lisp (use-package markdown-mode :straight t :mode "\\.md\\'" :config (setq markdown-command (concat "pandoc" " --from=markdown --to=html" " --standalone --mathjax --highlight-style=pygments" " --css=pandoc.css" " --quiet" )) (setq markdown-live-preview-delete-export 'delete-on-export) (setq markdown-asymmetric-header t) (setq markdown-open-command "/home/pavel/bin/scripts/chromium-sep") (add-hook 'markdown-mode-hook #'smartparens-mode) (general-define-key :keymaps 'markdown-mode-map "M-" 'markdown-promote "M-" 'markdown-demote)) ;; (use-package livedown ;; :straight (:host github :repo "shime/emacs-livedown") ;; :commands livedown-preview ;; :config ;; (setq livedown-browser "qutebrowser")) #+end_src *** 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)) :mode (rx (| "srt" "vtt" "ass") eos)) #+end_src *** LanguageTool LanguageTool is a great offline spell checker. For some reason, the download link is nowhere to be found on the home page, so it is listed in the references as well. References: - [[https://languagetool.org/][LanguageTool homepage]] - [[https://dev.languagetool.org/http-server][LanguageTool http server]] #+begin_src emacs-lisp (use-package langtool :straight t :commands (langtool-check) :config (setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-5.4/languagetool-server.jar") (setq langtool-mother-tongue "ru") (setq langtool-default-language "en-US")) (my-leader-def :infix "L" "" '(:which-key "languagetool") "c" 'langtool-check "s" 'langtool-server-stop "d" 'langtool-check-done "n" 'langtool-goto-next-error "p" 'langtool-goto-previous-error "l" 'langtool-correct-buffer) #+end_src ** Lisp [[./dot-imgs/lisp_cycles.png]] *** Meta Lisp Some packages for editing various Lisps. #+begin_src emacs-lisp (use-package lispy :commands (lispy-mode) :straight t) (use-package lispyville :hook (lispy-mode . lispyville-mode) :straight t) (sp-with-modes sp-lisp-modes (sp-local-pair "'" nil :actions nil)) #+end_src *** Emacs Lisp **** Package Lint A package that checks for the metadata in Emacs Lisp packages. #+begin_src emacs-lisp (use-package flycheck-package :straight t :after flycheck :config (flycheck-package-setup)) #+end_src **** General settings #+begin_src emacs-lisp (add-hook 'emacs-lisp-mode-hook #'aggressive-indent-mode) ;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode) (add-hook 'emacs-lisp-mode-hook #'lispy-mode) #+end_src *** 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 :mode "\\.clj[sc]?\\'" :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 :if (not my/lowpower) :commands (geiser run-geiser) :config (setq geiser-default-implementation 'guile)) (use-package geiser-guile :straight t :after geiser) (add-hook 'scheme-mode-hook #'aggressive-indent-mode) (add-hook 'scheme-mode-hook #'lispy-mode) #+end_src *** CLIPS An honorary Lisp #+begin_src emacs-lisp (use-package clips-mode :straight t :mode "\\.cl\\'" :config (add-hook 'clips-mode 'lispy-mode)) #+end_src ** Python Use [[https://github.com/Microsoft/python-language-server][Microsoft Language Server for Python]]. For some reason it doesn't use pipenv python executable, so here is a small workaround. #+begin_src emacs-lisp (setq my/pipenv-python-alist '()) (defun my/get-pipenv-python () (let ((default-directory (projectile-project-root))) (if (file-exists-p "Pipfile") (let ((asc (assoc default-directory my/pipenv-python-alist))) (if asc (cdr asc) (let ((python-executable (string-trim (shell-command-to-string "PIPENV_IGNORE_VIRTUALENVS=1 pipenv run which python 2>/dev/null")))) (if (string-match-p ".*not found.*" python-executable) (message "Pipfile found, but not pipenv executable!") (message (format "Found pipenv python: %s" python-executable)) (add-to-list 'my/pipenv-python-alist (cons default-directory python-executable)) python-executable)))) "python"))) (use-package lsp-pyright :straight t :defer t :if (not my/slow-ssh) :hook (python-mode . (lambda () (require 'lsp-pyright) (setq-local lsp-pyright-python-executable-cmd (my/get-pipenv-python)) (lsp)))) (add-hook 'python-mode-hook #'smartparens-mode) (add-hook 'python-mode-hook #'hs-minor-mode) #+end_src *** pipenv [[https://github.com/pypa/pipenv][Pipenv]] is a package manager for Python. Automatically creates & manages virtualenvs and stores data in =Pipfile= and =Pipfile.lock= (like npm's =package.json= and =package-lock.json=). #+begin_src emacs-lisp (use-package pipenv :straight t :hook (python-mode . pipenv-mode) :if (not my/slow-ssh) :init (setq pipenv-projectile-after-switch-function #'pipenv-projectile-after-switch-extended)) #+end_src *** 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) :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 *** 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 "rr" (lambda () (interactive) (unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p)) (py-isort-buffer)) (yapfify-buffer))) #+end_src *** sphinx-doc A package to generate sphinx-compatible docstrings. #+begin_src emacs-lisp (use-package sphinx-doc :straight t :hook (python-mode . sphinx-doc-mode) :config (my-leader-def :keymaps 'sphinx-doc-mode-map "rd" 'sphinx-doc)) #+end_src *** pytest [[https://docs.pytest.org/en/6.2.x/][pytest]] is a unit testing framework for Python. Once again a function to set pytest executable from pipenv. References: - [[https://docs.pytest.org/en/6.2.x/][pytest docs]] - [[https://github.com/wbolster/emacs-python-pytest][emacs-python-pytest]] #+begin_src emacs-lisp :noweb yes (defun my/set-pipenv-pytest () (setq-local python-pytest-executable (concat (my/get-pipenv-python) " -m pytest"))) (use-package python-pytest :straight t :commands (python-pytest-dispatch) :init (my-leader-def :keymaps 'python-mode-map :infix "t" "t" 'python-pytest-dispatch) :config <> (add-hook 'python-mode-hook #'my/set-pipenv-pytest) (when (derived-mode-p 'python-mode) (my/set-pipenv-pytest))) #+end_src **** Fix comint buffer width For some reason, the default comint output width is way too large. To fix that, I've modified the following function in the =python-pytest= package. #+begin_src emacs-lisp :noweb-ref override-pytest-run :tangle no (cl-defun python-pytest--run-as-comint (&key command) "Run a pytest comint session for COMMAND." (let* ((buffer (python-pytest--get-buffer)) (process (get-buffer-process buffer))) (with-current-buffer buffer (when (comint-check-proc buffer) (unless (or compilation-always-kill (yes-or-no-p "Kill running pytest process?")) (user-error "Aborting; pytest still running"))) (when process (delete-process process)) (let ((inhibit-read-only t)) (erase-buffer)) (unless (eq major-mode 'python-pytest-mode) (python-pytest-mode)) (compilation-forget-errors) (display-buffer buffer) (setq command (format "export COLUMNS=%s; %s" (- (window-width (get-buffer-window buffer)) 5) command)) (insert (format "cwd: %s\ncmd: %s\n\n" default-directory command)) (setq python-pytest--current-command command) (when python-pytest-pdb-track (add-hook 'comint-output-filter-functions 'python-pdbtrack-comint-output-filter-function nil t)) (run-hooks 'python-pytest-setup-hook) (make-comint-in-buffer "pytest" buffer "bash" nil "-c" command) (run-hooks 'python-pytest-started-hook) (setq process (get-buffer-process buffer)) (set-process-sentinel process #'python-pytest--process-sentinel)))) #+end_src *** code-cells Support for text with magic comments. #+begin_src emacs-lisp (use-package code-cells :straight t :commands (code-cells-mode)) #+end_src *** tensorboard A function to start up [[https://www.tensorflow.org/tensorboard][TensorBoard]]. #+begin_src emacs-lisp (setq my/tensorboard-buffer "TensorBoard-out") (defun my/tensorboard () (interactive) (start-process "tensorboard" my/tensorboard-buffer "tensorboard" "serve" "--logdir" (car (find-file-read-args "Directory: " t))) (display-buffer my/tensorboard-buffer)) #+end_src ** Data serialization *** JSON #+begin_src emacs-lisp (use-package json-mode :straight t :mode "\\.json\\'" :config (add-hook 'json-mode #'smartparens-mode) (add-hook 'json-mode #'hs-minor-mode) (my/set-smartparens-indent 'json-mode)) #+end_src *** CSV #+begin_src emacs-lisp (use-package csv-mode :straight t :mode "\\.csv\\'") #+end_src *** YAML #+begin_src emacs-lisp (use-package yaml-mode :straight t :mode "\\.yml\\'" :config (add-hook 'yaml-mode-hook 'smartparens-mode) (add-hook 'yaml-mode-hook 'highlight-indent-guides-mode) (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))) #+end_src ** Configuration *** .env #+begin_src emacs-lisp (use-package dotenv-mode :straight t :mode "\\.env\\..*\\'") #+end_src *** .gitignore A package to quickly create =.gitignore= files. #+begin_src emacs-lisp (use-package gitignore-templates :straight t :commands (gitignore-templates-insert gitignore-templates-new-file)) #+end_src *** Docker #+begin_src emacs-lisp (use-package dockerfile-mode :mode "Dockerfile\\'" :straight t :config (add-hook 'dockerfile-mode 'smartparens-mode)) #+end_src *** crontab #+begin_src emacs-lisp (use-package crontab-mode :straight t) #+end_src ** Shell *** sh #+begin_src emacs-lisp (add-hook 'sh-mode-hook #'smartparens-mode) #+end_src *** fish #+begin_src emacs-lisp (use-package fish-mode :straight t :mode "\\.fish\\'" :config (add-hook 'fish-mode-hook #'smartparens-mode)) #+end_src ** Java #+begin_src emacs-lisp (use-package lsp-java :straight t :after (lsp) :config (setq lsp-java-jdt-download-url "https://download.eclipse.org/jdtls/milestones/0.57.0/jdt-language-server-0.57.0-202006172108.tar.gz")) (add-hook 'java-mode-hook #'smartparens-mode) ;; (add-hook 'java-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'java-mode) #+end_src ** Go #+begin_src emacs-lisp (use-package go-mode :straight t :mode "\\.go\\'" :config (my/set-smartparens-indent 'go-mode) (add-hook 'go-mode-hook #'smartparens-mode) (add-hook 'go-mode-hook #'hs-minor-mode)) #+end_src ** .NET *** C# | Guix dependencies | Disabled | |-------------------+----------| | omnisharp | t | | dotnet | t | #+begin_src emacs-lisp (use-package csharp-mode :straight t :mode "\\.cs\\'" :config (setq lsp-csharp-server-path (executable-find "omnisharp-wrapper")) (add-hook 'csharp-mode-hook #'csharp-tree-sitter-mode) (add-hook 'csharp-tree-sitter-mode-hook #'smartparens-mode) (add-hook 'csharp-mode-hook #'hs-minor-mode) (my/set-smartparens-indent 'csharp-tree-sitter-mode)) #+end_src *** MSBuild #+begin_src emacs-lisp (use-package csproj-mode :straight t :mode "\\.csproj\\'" :config (add-hook 'csproj-mode #'smartparens-mode)) #+end_src ** Haskell #+begin_src emacs-lisp (use-package haskell-mode :straight t :mode "\\.hs\\'") (use-package lsp-haskell :straight t :after (lsp haskell-mode)) #+end_src ** Lua #+begin_src emacs-lisp (use-package lua-mode :straight t :mode "\\.lua\\'" :hook (lua-mode . smartparens-mode)) (my/set-smartparens-indent 'lua-mode) #+end_src ** 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, "-u")) (my-leader-def :keymaps '(sql-mode-map) "rr" #'sqlformat-buffer) #+end_src ** SPARQL #+begin_src emacs-lisp (use-package sparql-mode :straight t) #+end_src * Org Mode *Org mode* is a tool that leverages plain-text files for various 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. #+begin_src emacs-lisp :noweb yes (use-package org :straight t :if (not my/remote-server) :defer t :init (setq org-directory (expand-file-name "~/Documents/org-mode")) :config (setq org-startup-indented t) (setq org-return-follows-link t) (setq org-src-tab-acts-natively nil) (add-hook 'org-mode-hook 'smartparens-mode) (add-hook 'org-agenda-mode-hook (lambda () (visual-line-mode -1) (toggle-truncate-lines 1) (display-line-numbers-mode 0))) (add-hook 'org-mode-hook (lambda () (rainbow-delimiters-mode -1))) (require 'org-tempo) (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")) (add-to-list 'org-structure-template-alist '("py" . "src python")) (add-to-list 'org-structure-template-alist '("sq" . "src sql")) <> (unless my/is-termux <>) <> <> <>) #+end_src *** Encryption Setting up =org-crypt= to encrypt a part of a file. #+begin_src emacs-lisp :noweb-ref org-crypt-setup (require 'org-crypt) (org-crypt-use-before-save-magic) (setq org-tags-exclude-from-inheritance (quote ("crypt"))) (setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205") #+end_src This enables encryption for Org segments which are tagged =:crypt:=. Another way to encrypt org files is to save them with extension =.org.gpg=. That way by default epa always prompts for a key, which is not what I want when there is in fact only one key to select. So I make the following advice: #+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)) (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 - =ol-notmuch= - integration with notmuch #+begin_src emacs-lisp (use-package org-contrib :straight (org-contrib :type git :host nil :repo "https://git.sr.ht/~bzg/org-contrib" :build t) :after (org) :config (require 'ox-extra) (require 'ol-notmuch) (ox-extras-activate '(latex-header-blocks ignore-headlines))) #+end_src ** Integration with evil A package to add more evil-mode keybindings to org-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 ** Literate programing *** Python & Jupyter Use jupyter kernels for Org Mode. References: - [[https://github.com/nnicandro/emacs-jupyter][emacs-jupyter repo]] - [[https://github.com/jkitchin/scimax/blob/master/scimax.org][SCIMAX manual]] #+begin_src emacs-lisp :noweb-ref org-lang-setup (use-package jupyter :straight t :after (org) :if (not my/is-termux) :init (my-leader-def "ar" 'jupyter-run-repl)) #+end_src Refresh kernelspecs. Kernelspecs by default are hashed, so even switching Anaconda environments doesn't change the kernel (i.e. kernel from the first environment is run after the switch to the second one). #+begin_src emacs-lisp (defun my/jupyter-refresh-kernelspecs () "Refresh Jupyter kernelspecs" (interactive) (jupyter-available-kernelspecs t)) #+end_src Also, if some kernel wasn't present at the moment of the load of =emacs-jupyter=, it won't be added to the =org-src-lang-modes= list. E.g. I have Hy kernel installed in a separate Anaconda environment, so if Emacs hasn't been launched in this environment, I wouldn't be able to use =hy= in org-src blocks. Fortunately, =emacs-jupyter= provides a function for that problem as well. #+begin_src emacs-lisp (defun my/jupyter-refesh-langs () "Refresh Jupyter languages" (interactive) (org-babel-jupyter-aliases-from-kernelspecs t)) #+end_src *** Hy #+begin_src emacs-lisp :noweb-ref org-lang-setup (use-package ob-hy :after (org) :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 :tangle no :noweb-ref org-lang-setup (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 *** Setup Enable languages #+begin_src emacs-lisp :tangle no :noweb-ref org-lang-setup (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (python . t) (sql . t) ;; (typescript .t) (hy . t) (shell . t) (plantuml . t) (octave . t) (jupyter . t) (sparql . t))) (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images) #+end_src Use Jupyter block instead of built-in Python. #+begin_src emacs-lisp :tangle no :noweb-ref org-lang-setup (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 :tangle no :noweb-ref org-lang-setup (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"))) #+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 *** 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))) (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 *** Managing a literate programming project A few tricks to do literate programming. I prefer to put the org files to a separate directory (e.g. =org=). So I've come up with the following solution to avoid manually prefixing the =:tangle= arguments. Set up the following argument with the path to the project root: #+begin_example #+PROPERTY: PRJ-DIR .. #+end_example A function to do the prefixing: #+begin_src emacs-lisp (defun my/org-prj-dir (path) (expand-file-name path (org-entry-get nil "PRJ-DIR" t))) #+end_src Example usage is as follows: #+begin_example :tangle (my/org-prj-dir "sqrt_data/api/__init__.py") #+end_example ** Tools Various small packages. *** Presentations Doing presentations with [[https://github.com/rlister/org-present][org-present]]. #+begin_src emacs-lisp (use-package hide-mode-line :straight t :after (org-present)) (defun my/present-next-with-latex () (interactive) (org-present-next) (org-latex-preview '(16))) (defun my/present-prev-with-latex () (interactive) (org-present-prev) (org-latex-preview '(16))) (use-package org-present :straight (:host github :repo "rlister/org-present") :if (not my/remote-server) :commands (org-present) :config (general-define-key :keymaps 'org-present-mode-keymap "" 'my/present-next-with-latex "" 'my/present-prev-with-latex) (add-hook 'org-present-mode-hook (lambda () (blink-cursor-mode 0) (org-present-big) ;; (org-display-inline-images) (org-present-hide-cursor) (org-present-read-only) (display-line-numbers-mode 0) (hide-mode-line-mode +1) (setq-local org-format-latex-options (plist-put org-format-latex-options :scale (* org-present-text-scale my/org-latex-scale 0.5))) (org-latex-preview '(16)))) (add-hook 'org-present-mode-quit-hook (lambda () (blink-cursor-mode 1) (org-present-small) ;; (org-remove-inline-images) (org-present-show-cursor) (org-present-read-write) (display-line-numbers-mode 1) (hide-mode-line-mode 0) (setq-local org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale)) (org-latex-preview '(64))))) #+end_src *** TOC Make a TOC inside the org file. References: - [[https://github.com/alphapapa/org-make-toc][alphapapa/org-make-toc]] #+begin_src emacs-lisp (use-package org-make-toc :after (org) :if (not my/remote-server) :commands (org-make-toc org-make-toc-insert org-make-toc-set org-make-toc-at-point) :straight t) #+end_src *** Screenshots A nice package to make screenshots and insert them to the Org document. #+begin_src emacs-lisp (use-package org-attach-screenshot :commands (org-attach-screenshot) :straight t) #+end_src *** Transclusion A package that implements transclusions in Org Mode, that is rendering a part of one file inside of 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 ** Productivity & Knowledge management My ongoing effort to get a productivity setup in Org. Some inspiration: - [[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]] Used files #+begin_src emacs-lisp :tangle no :noweb-ref org-productivity-setup (setq org-roam-directory (concat org-directory "/roam")) (setq org-agenda-files '("inbox.org")) ;; (setq org-default-notes-file (concat org-directory "/notes.org")) #+end_src Hotkeys #+begin_src emacs-lisp (my-leader-def :infix "o" "" '(:which-key "org-mode") "c" 'org-capture "a" 'org-agenda) #+end_src Refile targets #+begin_src emacs-lisp (setq org-refile-targets '()) (setq org-refile-use-outline-path 'file) (setq org-outline-path-complete-in-steps nil) #+end_src *** Capture templates & various settings 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.org" org-directory (format-time-string "%Y%m%d%H%M%S"))) (setq org-capture-templates `(("i" "Inbox" entry (file "inbox.org") ,(concat "* TODO %?\n" "/Entered on/ %U")) ("e" "email" entry (file "inbox.org") ,(concat "* TODO %:from %:subject \n" "/Entered on/ %U\n" "/Received on/ %:date-timestamp-inactive\n" "%a\n")) ("f" "elfeed" entry (file "inbox.org") ,(concat "* TODO %:elfeed-entry-title\n" "/Entered on/ %U\n" "%a\n")) ("n" "note" entry (file my/generate-inbox-note-name) ,(concat "* %?\n" "/Entered on/ %U")))) #+end_src Effort estimation #+begin_src emacs-lisp :tangle no :noweb-ref org-productivity-setup (add-to-list 'org-global-properties '("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 2:00 4:00")) #+end_src Log DONE time #+begin_src emacs-lisp :tangle no :noweb-ref org-productivity-setup (setq org-log-done 'time) #+end_src *** Trello sync Some of the projects I'm participating in are managed via Trello, so I use [[http://org-trello.github.io/][org-trello]] to keep track of them. The package has a remarkably awkward keybindings setup, so my effort to call =my-leader-def= to set keybindings I like is no less awkward. Also, trello files are huge and have a lot of information and tasks which do not concern me, so I don't add them to =org-agenda-files=. #+begin_src emacs-lisp (setq org-trello-files (thread-last (concat org-directory "/trello") (directory-files) (seq-filter (lambda (f) (string-match-p (rx ".org" eos) f))) (mapcar (lambda (f) (concat org-directory "/trello/" f))))) #+end_src #+begin_src emacs-lisp (use-package org-trello :straight (:build (:not native-compile)) :commands (org-trello-mode) :init (setq org-trello-current-prefix-keybinding "C-c o") (setq org-trello-add-tags nil) (add-hook 'org-mode-hook (lambda () (when (string-match-p (rx "trello") (or (buffer-file-name) "")) (org-trello-mode)))) :config (eval `(my-leader-def :infix "o t" :keymaps '(org-trello-mode-map) "" '(:which-key "trello") ,@(mapcan (lambda (b) (list (nth 1 b) (macroexp-quote (nth 0 b)))) org-trello-interactive-command-binding-couples)))) #+end_src *** org-ql [[https://github.com/alphapapa/org-ql][org-ql]] is a package to query the org files. I'm using it in my review workflow and for custom agenda views. #+begin_src emacs-lisp :tangle no :noweb-ref org-productivity-setup (use-package org-ql :straight (:fetcher github :repo "alphapapa/org-ql" :files (:defaults (:exclude "helm-org-ql.el")))) #+end_src *** Custom agendas Some custom agendas to fit my workflow. Despite the fact that I don't add =org-trello-files= to =org-agenda-files= I still want to see them in agenda, so I use =org-ql-block= from =org-ql=. #+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" "log" "log_here"))) (setq org-agenda-custom-commands `(("p" "My outline" ((agenda "") (todo "NEXT" ((org-agenda-prefix-format " %i %-12:c [%e] ") (org-agenda-overriding-header "Next tasks"))) (org-ql-block `(and (regexp ,(rx ":orgtrello_users:" (* nonl) "sqrtminusone")) (todo) (deadline)) ((org-agenda-files ',org-trello-files) (org-ql-block-header "Trello assigned"))) (tags-todo "inbox" ((org-agenda-overriding-header "Inbox") (org-agenda-prefix-format " %i %-12:c") (org-agenda-hide-tags-regexp "."))) (tags-todo "+waitlist+SCHEDULED<=\"<+14d>\"" ((org-agenda-overriding-header "Waitlist") (org-agenda-hide-tags-regexp "waitlist") (org-agenda-prefix-format " %i %-12:c %-12(my/org-scheduled-get-time)"))))) ("tp" "Personal tasks" ((tags-todo "personal" ((org-agenda-prefix-format " %i %-12:c [%e] "))))))) #+end_src *** Org Journal [[https://github.com/bastibe/org-journal][org-journal]] is a plugin for maintaining a journal in org mode. I want to have its entries separate from my knowledge base. #+begin_src emacs-lisp (use-package org-journal :straight t :if (not my/remote-server) :after org :config (setq org-journal-dir (concat org-directory "/journal")) (setq org-journal-file-type 'weekly) (setq org-journal-file-format "%Y-%m-%d.org") (setq org-journal-date-format "%A, %Y-%m-%d") (setq org-journal-enable-encryption t)) (my-leader-def :infix "oj" "" '(:which-key "org-journal") "j" 'org-journal-new-entry "o" 'org-journal-open-current-journal-file "s" 'org-journal-search) #+end_src Also, I want to store some information in the journal as properties of the record. So below is a function that does just that. As of now, it stores Emacs version, hostname, location, and current EMMS track if there is one. #+begin_src emacs-lisp (defun my/set-journal-header () (org-set-property "Emacs" emacs-version) (org-set-property "Hostname" system-name) (when (boundp 'my/location) (org-set-property "Location" my/location)) (when (fboundp 'emms-playlist-current-selected-track) (let ((track (emms-playlist-current-selected-track))) (when track (let ((album (cdr (assoc 'info-album track))) (artist (or (cdr (assoc 'info-albumartist track)) (cdr (assoc 'info-album track)))) (title (cdr (assoc 'info-title track))) (string "")) (when artist (setq string (concat string "[" artist "] "))) (when album (setq string (concat string album " - "))) (when title (setq string (concat string title))) (when (> (length string) 0) (org-set-property "EMMS_Track" string))))))) (add-hook 'org-journal-after-entry-create-hook #'my/set-journal-header) #+end_src *** Org Roam [[https://github.com/org-roam/org-roam][org-roam]] is a plain-text knowledge database. **** Basic package configuration | Guix dependency | |-----------------------| | emacs-emacsql-sqlite3 | | graphviz | References: - [[https://github.com/org-roam/org-roam/wiki/Hitchhiker%27s-Rough-Guide-to-Org-roam-V2][Hitchhiker's Rough Guide to Org roam V2]] #+begin_src emacs-lisp (use-package emacsql-sqlite :defer t :straight (:type built-in)) (use-package org-roam :straight (:host github :repo "org-roam/org-roam" :files (:defaults "extensions/*.el")) :if (not my/remote-server) :after org :init (setq org-roam-file-extensions '("org")) (setq org-roam-v2-ack t) (setq orb-insert-interface 'ivy-bibtex) :config (org-roam-setup) (require 'org-roam-protocol)) #+end_src **** Capture templates and dailies Capture templates for =org-roam-capture=. As for now, nothing too complicated here. #+begin_src emacs-lisp (setq my/org-roam-project-template `("p" "project" plain ,(string-join '("%?" "* Tasks" "** TODO Add initials tasks" "* Log :log_here:") "\n") :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" ,(string-join '("#+title: ${title}" "#+category: ${title}" "#+filetags: :org:log_here:" "#+TODO: TODO(t) NEXT(n) HOLD(h) | NO(q) DONE(d)" "#+TODO: FUTURE(f) | PASSED(p)" "#+STARTUP: logdone overview") "\n")) :unnarrowed t)) (setq org-roam-capture-templates `(("d" "default" plain "%?" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n") :unnarrowed t) ("e" "encrypted" plain "%?" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n") :unnarrowed t) ,my/org-roam-project-template)) #+end_src **** Org Roam Dailies =org-roam-dailies= provides journaling capabilities similar to [[https://github.com/bastibe/org-journal][Org Journal]]. I was using the latter for half a year or so but decided to try to use Org Roam for this purpose. Org Roam has an advantage that, well, its entries form a database, which means I can look for backlinks to the journal and such. I'd like to have my entries encrypted, so I save them with =.org.gpg=. Org Roam still caches them in plaintext though, but at least that way records can be safely pushed to a repository. Similar to my previous workflow with Org Journal, I want to have some information on what is happening when I make a record. So, here is a function to figure out that a track played by EMMS: #+begin_src emacs-lisp (defun my/make-daily-header-track () (when (fboundp 'emms-playlist-current-selected-track) (let ((track (emms-playlist-current-selected-track))) (when track (let ((album (cdr (assoc 'info-album track))) (artist (or (cdr (assoc 'info-albumartist track)) (cdr (assoc 'info-album track)))) (title (cdr (assoc 'info-title track))) (string "")) (when artist (setq string (concat string "[" artist "] "))) (when album (setq string (concat string album " - "))) (when title (setq string (concat string title))) (when (> (length string) 0) string)))))) #+end_src And here is a function to make a corresponding property drawer. #+begin_src emacs-lisp (defun my/make-daily-header () (string-join (seq-filter #'identity `(":PROPERTIES:" ,(format ":Emacs: %s" emacs-version) ,(format ":Hostname: %s" system-name) ,(when (boundp 'my/location) (format ":Location: %s" my/location)) ,(when-let (track (my/make-daily-header-track)) (format ":EMMS_Track: %s" track)) ":END:")) "\n")) #+end_src Now we can define a capture template for =org-roam-dailies=. This one creates one file for one day; I tried to make something like one per week, but apparently, Org Roam is keen to assign one ID for one file, which is not quite what I want. #+begin_src emacs-lisp (setq org-roam-dailies-capture-templates `(("d" "default" entry ,(string-join '("* %<%H:%M>" "%(my/make-daily-header)" "%?") "\n") :target (file+head "%<%Y-%m-%d>.org.gpg" ,(string-join '("#+TITLE: %<%Y-%m-%d, %A>" "#+FILETAGS: log" "") "\n"))))) #+end_src **** Managing projects in Org Roam Org Roam is also pretty good for managing projects. - *CREDIT*: Thanks to David Wilson [[https://systemcrafters.net/build-a-second-brain-in-emacs/5-org-roam-hacks/][for ideas]] ***** Integration with Org Agenda Because projects usually have various timestamps and deadlines, I want the project files to populate my =org-agenda-files=. So here I define a simple macro to filter the Roam notes list from unwanted entries. #+begin_src emacs-lisp (cl-defmacro my/org-roam-filter-by-tag (&optional &key (include nil) (exclude nil)) `(lambda (node) (let ((tags (org-roam-node-tags node))) (and ,(if include `(or ,@(mapcar (lambda (tag) `(member ,tag tags)) include)) t) ,@(mapcar (lambda (tag) `(not (member ,tag tags))) exclude))))) (defun my/org-roam-list-notes-by-tag (tag-name) (mapcar #'org-roam-node-file (seq-filter (my/org-roam-filter-by-tag :include (tag-name)) (org-roam-node-list)))) #+end_src Now, let's integrate the found project notes to the rest of Org Mode. Besides =org-agenda-files=, I want them to serve as refile targets, so here is a function that updated both variables. That function is called on initialization after the initial values of both variables are set, but can also be called manually. #+begin_src emacs-lisp (defun my/org-roam-refresh-agenda-list () (interactive) (let ((project-files (my/org-roam-list-notes-by-tag "org"))) (setq org-agenda-files (seq-uniq `(,@org-agenda-files ,@project-files))) (dolist (file project-files) (add-to-list 'org-refile-targets `(,file :tag . "refile")) (add-to-list 'org-refile-targets `(,file :regexp . ,(rx (or "Tasks"))))))) (with-eval-after-load 'org-roam (my/org-roam-refresh-agenda-list)) #+end_src David Wilson has proposed a function to automatically update =org-agenda-buffers= after capturing a project, but that's a bit too complicated to pull off correctly. ***** Capturing tasks and projects Find or capture a project. #+begin_src emacs-lisp (defun my/org-roam-find-project () (interactive) (org-roam-node-find nil nil (my/org-roam-filter-by-tag :include ("org")) :templates `(,my/org-roam-project-template))) #+end_src ***** Targeting refile Because in the previous section I've added a lot of stuff to the =org-refile-targets=, the list given by =org-refile= becomes a bit too large. So let's make a function that performs a sort of two-step refile: #+begin_src emacs-lisp (defun my/org-target-refile (&optional arg) (interactive "P") (let* ((selected-file (completing-read "Refile to: " (seq-uniq (mapcar #'car org-refile-targets)))) (org-refile-targets (cl-loop for target in org-refile-targets if (string-equal (car target) selected-file) collect target))) (org-refile-cache-clear) (org-refile arg))) (general-define-key :keymaps 'org-mode-map "C-c C-w" #'my/org-target-refile) #+end_src **** Automatic transclusion for Dailies I've been using org-journal for quite some time, and while it's great, I don't like its linear structure too much. I have all kinds of stuff written there - things related to my job, various personal projects, etc, and it's pretty hard to query a specific thing. Now that I migrated all the things I've been working on to Org Roam, I can reference some Roam nodes and check backlinks for a particular node, e.g. for a project. But even that isn't quite good enough, because I'd also like to search the references and somehow see the list of records related just to this particular node. So, I've come up with the following solution. I'll use the following syntax in the daily note: #+begin_example []: #+end_example And if that node has a filetag =log_here=, this line will be transcluded under a particular header in that node: #+begin_example ,* :log_here: ... ,** ,#+transclude: #+end_example So, first, we have to extract all such links from the daily file. The function below will return a list of items like =(node line-number line-count title)= #+begin_src emacs-lisp (defun my/org-roam-daily-extract-target-links () (save-excursion (goto-char (point-min)) (cl-loop while (not (eobp)) do (forward-line 1) for match = (save-excursion (search-forward-regexp (rx "[" (* nonl) "]") (line-end-position) t)) for node = (when-let (link (org-element-link-parser)) (when (string-equal (plist-get (cadr link) :type) "id") (when-let (node (org-roam-node-from-id (plist-get (cadr link) :path))) (and (member "log_here" (org-roam-node-tags node)) node)))) if (and node match) collect (list node (line-number-at-pos) ;; Hardcoded for now 1 (let ((title (save-excursion (org-back-to-heading) (org-element-property :title (org-element-at-point))))) (if (stringp title) title (substring-no-properties (car title)))))))) #+end_src And this is a good point to introduce a function that inserts a link which can be found by the function above: #+begin_src emacs-lisp (defun my/org-roam-node-insert-log () (interactive) (beginning-of-line) (org-roam-node-insert (my/org-roam-filter-by-tag :include ("log_here"))) (insert ": ")) #+end_src Second, insert all these links to the target node. The daily header is just a title of the current node: #+begin_src emacs-lisp (defun my/org-roam-daily-get-transclude-header () (let ((kws (org-collect-keywords '("TITLE")))) (unless kws (error "No title found!")) (cadar kws))) #+end_src This daily header should be inserted under the log header, which is a header with =log_here= tag. Let's make a function to find that header: #+begin_src emacs-lisp (defun my/org-roam-daily-find-log-header () (let ((log-header nil)) (org-map-entries (lambda () (let* ((headline (org-element-at-point)) (tags (mapcar #'substring-no-properties (org-element-property :tags headline)))) (when (member "log_here" tags) (setq log-header headline))))) (unless log-header (error "Header with :log_here: tag not found")) log-header)) #+end_src Here is a bit of craziness. I want the list of daily headers to be ordered alphabetically. So, I need a function that inserts a header while preserving the alphabetical order. #+begin_src emacs-lisp (defun my/org-insert-alphabetical-header (root-header target-header) (let ((target-headline) (last-header "") (last-headline) (first-headline)) (goto-char (org-element-property :begin root-header)) ;; Map the tree under root-header (org-map-tree (lambda () (let* ((headline (org-element-at-point)) (header (org-element-property :title headline))) (when (/= (point) (org-element-property :begin root-header)) ;; Try to find a heading with title equal to target-header (when (string-equal target-header header) (setq target-headline headline)) ;; Or try to find a heading < target-header (when (and (string-lessp header target-header) (string-greaterp header last-header)) (setq last-header header) (setq last-headline headline)) (unless first-headline (setq first-headline headline)))))) (if target-headline ;; If a matching header is found, clear its contents (let ((content-start (save-excursion (goto-char (org-element-property :begin target-headline)) (forward-line 1) (point))) (content-end (org-element-property :end target-headline))) (if (<= content-start content-end) (progn (delete-region content-start content-end) (goto-char (org-element-property :begin target-headline)) (end-of-line) (insert "\n")) (goto-char content-end))) ;; If a heading < target-header is found, insert the new one just after it (if last-headline (progn (goto-char (org-element-property :begin last-headline)) (org-insert-heading-respect-content) (insert target-header) (insert "\n")) ;; If neither is found, insert a new heading before the first headline (if first-headline (progn (goto-char (org-element-property :begin first-headline)) (org-insert-heading) (insert target-header) (insert "\n")) ;; If there is not even a first heading, that means the target header is empty (goto-char (org-element-property :begin root-header)) (org-insert-heading-respect-content) (org-do-demote) (insert target-header) (insert "\n")))))) #+end_src With that out of the way, we can format the list generated by =my/org-roam-daily-extract-target-links=: #+begin_src emacs-lisp (defun my/org-roam-daily-format-target-links (links path) (string-trim (cl-loop for i from 0 to (length links) for link in links for line-number = (nth 1 link) for line-count = (nth 2 link) for title = (nth 3 link) for prev-title = (if (> i 0) (nth 3 (nth (1- i) links)) "") concat (string-join (seq-filter #'identity `(,(unless (string-equal title prev-title) (format "%s:" title)) ,(format "#+transclude: [[file:%s]] :lines %d-%d" path line-number (+ line-number line-count -1)) "" "")) "\n")))) #+end_src Put it all together. Yay! #+begin_src emacs-lisp (defun my/org-roam-daily-dispatch-transclusions () (interactive) (let* ((targets (my/org-roam-daily-extract-target-links)) (target-groups (seq-group-by (lambda (item) (org-roam-node-file (nth 0 item))) targets)) (header (my/org-roam-daily-get-transclude-header)) (path (buffer-file-name))) (dolist (group target-groups) (with-temp-file (car group) (insert-file-contents (car group)) (org-mode) (my/org-insert-alphabetical-header (my/org-roam-daily-find-log-header) header) (insert (my/org-roam-daily-format-target-links (cdr group) path)))))) #+end_src Finally, let's put this function into the save hook: #+begin_src emacs-lisp (defun my/org-roam-daily-transclusions-hook () (when (and (fboundp 'org-roam-dailies--daily-note-p) (org-roam-dailies--daily-note-p)) (my/org-roam-daily-dispatch-transclusions) (message "Tranclusions dispatched!"))) (with-eval-after-load 'org-roam (add-hook 'after-save-hook #'my/org-roam-daily-transclusions-hook)) #+end_src **** Keybindings A set of keybindings to quickly access things in Org Roam. As of now, I have 3 categories of nodes in Org Roam: - dailies (tag =log=) - project notes (tag =org=) - Zettelkasten, which is everything else. So I want to have a separate fuzzy search for every category. =my/org-roam-find-project= for project nodes is already defined above, here is the rest: #+begin_src emacs-lisp (defun my/org-roam-find-zk () (interactive) (org-roam-node-find nil nil (my/org-roam-filter-by-tag :exclude ("log" "org")))) (defun my/org-roam-find-daily () (interactive) (org-roam-node-find nil nil (my/org-roam-filter-by-tag :include ("log")))) #+end_src And here are keybindings. #+begin_src emacs-lisp (my-leader-def :infix "or" "" '(:which-key "org-roam") "i" 'org-roam-node-insert "r" '(my/org-roam-find-zk :wk "ZK") "p" '(my/org-roam-find-project :wk "Projects") "g" 'org-roam-graph "c" 'org-roam-capture "b" 'org-roam-buffer-toggle) (my-leader-def :infix "od" "" '(:which-key "org-roam-dailies") "d" #'org-roam-dailies-capture-today "o" #'org-roam-dailies-goto-today "f" #'my/org-roam-find-daily "i" #'my/org-roam-node-insert-log) (with-eval-after-load 'org-roam (general-define-key :keymaps 'org-roam-mode-map :states '(normal) "TAB" #'magit-section-toggle "q" #'quit-window "k" #'magit-section-backward "j" #'magit-section-forward "gr" #'revert-buffer "RET" #'org-roam-buffer-visit-thing)) (with-eval-after-load 'org (my-leader-def :keymap 'org-mode-map :infix "or" "t" 'org-roam-tag-add "T" 'org-toam-tag-remove) (general-define-key :keymap 'org-mode-map "C-c i" 'org-id-get-create "C-c l o" 'org-roam-node-insert)) #+end_src **** Org Roam UI A browser frontend to visualize a Roam directory in a form of a graph. #+begin_src emacs-lisp (use-package org-roam-ui :straight (:host github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out")) :after org-roam ;; :hook (org-roam . org-roam-ui-mode) :init (my-leader-def "oru" #'org-roam-ui-mode)) #+end_src **** Org Roam Protocol Open links such as =org-protocol://= from browser. Run =M-x server-start= for org-protocol to work. #+begin_src conf :tangle ~/.local/share/applications/org-protocol.desktop [Desktop Entry] Name=Org-Protocol Exec=emacsclient %u Icon=emacs-icon Type=Application Terminal=false MimeType=x-scheme-handler/org-protocol #+end_src Don't forget to run the following after setup: #+begin_src bash :tangle no xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol #+end_src *** Review workflow My take on a review workflow. As a baseline, I want to have a template that lists the important changes since the last review and other basic information. I'm doing reviews regularly, but the time intervals still may vary, hence this flexibility. This section has seen some updates over time. **** Data from git First, as I have [[file:Console.org::=autocommit=][autocommit]] set up in my org directory, here is a handy function to get an alist of changed files of a form =(status . path)=. In principle, the =rev= parameter can be a commit, tag, etc but here I'm interested in a form like =@{2021-08-30}=. Also in principle, Org Roam DB also stores stuff like creation time and modification time, but I started this section before I started using Org Roam extensively, so git works fine for me. #+begin_src emacs-lisp (setq my/git-diff-status '(("A" . added) ("C" . copied) ("D" . deleted) ("M" . modified) ("R" . renamed) ("T" . type-changed) ("U" . unmerged))) (defun my/get-files-status (rev) (let ((files (shell-command-to-string (concat "git diff --name-status " rev)))) (mapcar (lambda (file) (let ((elems (split-string file "\t"))) (cons (cdr (assoc (car elems) my/git-diff-status)) (nth 1 elems)))) (split-string files "\n" t)))) #+end_src I'll use it to get a list of added and changed files in the Org directory since the last review. The date should be in a format =YYYY-MM-DD=. #+begin_src emacs-lisp (defun my/org-changed-files-since-date (date) (let ((default-directory org-directory)) (my/get-files-status (format "@{%s}" date)))) #+end_src **** Data from org-roam Now that we have the list of new & changed files, I want to sort into a bunch of categories: projects, log entries, etc. The categories are defined by tags. So here is a list of plists that sets these categories. The properties are as follows: - =:status= is a git status for the file - =:tags= is a plist that sets up the following conditions for the Roam node - =:include= - should be empty or one of these should be present - =:exclude= - should be empty or none of these should be present - =:title= is the name of category as I want it to be seen in the review template #+begin_src emacs-lisp (setq my/org-review-roam-queries '((:status added :tags (:include ("org")) :title "New Project Entries") (:status changed :tags (:include ("org")) :title "Changed Project Entries") (:status added :tags (:include ("log") :exclude ("org" "log_here")) :title "New Dailies") (:status added :tags (:exclude ("log" "org")) :title "New Zettelkasten Entries") (:status changed :tags (:exclude ("log" "org")) :title "Changed Zettelkasten Entries"))) #+end_src This list is used to extract & format the relevant section of the review template. =cl-loop= seems pretty good as a control flow structure, but I'll see if it is also pretty good at producing poorly maintainable code. At least at the moment of this writing, the function below looks rather concise. #+begin_src emacs-lisp (defun my/org-review-format-roam (changes) (cl-loop for query in my/org-review-roam-queries with nodes = (org-roam-node-list) with node-tags = (mapcar #'org-roam-node-tags nodes) for include-tags = (plist-get (plist-get query :tags) :include) for exclude-tags = (plist-get (plist-get query :tags) :exclude) ;; List of nodes filtered by :tags in query for filtered-nodes = (cl-loop for node in nodes for tags in node-tags if (and (or (seq-empty-p include-tags) (seq-intersection include-tags tags)) (or (seq-empty-p exclude-tags) (not (seq-intersection exclude-tags tags)))) collect node) ;; List of changes filtered by :status in query for filtered-changes = (cl-loop for change in changes if (and (eq (car change) (plist-get query :status)) (string-match-p (rx bos "roam") (cdr change))) collect (cdr change)) ;; Intersection of the two filtered lists for final-nodes = (cl-loop for node in filtered-nodes for path = (file-relative-name (org-roam-node-file node) org-directory) if (member path filtered-changes) collect node) ;; If the intersction list is not empty, format it to the result if final-nodes concat (format "** %s\n" (plist-get query :title)) ;; FInal list of links, sorted by title and concat (cl-loop for node in (seq-sort (lambda (node1 node2) (string-lessp (org-roam-node-title node1) (org-roam-node-title node2))) final-nodes) concat (format "- [[id:%s][%s]]\n" (org-roam-node-id node) (org-roam-node-title node))))) #+end_src **** Data from org-agenda via org-ql +Third+ second, I want to list some changes in my agenda. This section will change depending on what I'm currently working on. So, here is a list of queries results of which I want to see in the review template. The format is =(name date-field order-by-field query)=. #+begin_src emacs-lisp (setq my/org-ql-review-queries `(("Waitlist" scheduled scheduled (and (done) (tags-inherited "waitlist"))) ("Personal tasks done" closed ,nil (and (tags-inherited "personal") (todo "DONE"))) ("Attended meetings" closed scheduled (and (tags-inherited "meeting") (todo "PASSED"))) ("Done project tasks" closed deadline (and (todo "DONE") (ancestors (heading "Tasks")))))) #+end_src The query will be executed like this: =(and (date-field :from rev-date) query)= #+begin_src emacs-lisp (defun my/org-review-exec-ql (saved rev-date) (let ((query `(and (,(nth 1 saved) :from ,rev-date) ,(nth 3 saved)))) (org-ql-query :select #'element :from (org-agenda-files) :where query :order-by (nth 2 saved)))) #+end_src Format one element of the query result. #+begin_src emacs-lisp (defun my/org-review-format-element (elem) (concat (string-pad (plist-get (cadr elem) :raw-value) 40) (when-let (scheduled (plist-get (cadr elem) :scheduled)) (concat " [SCHEDULED: " (plist-get (cadr scheduled) :raw-value) "]")) (when-let (deadline (plist-get (cadr elem) :deadline)) (concat " [DEADLINE: " (plist-get (cadr deadline) :raw-value) "]")))) #+end_src Execute all the saved queries and format an Org list for the capture template. #+begin_src emacs-lisp (defun my/org-review-format-queries (rev-date) (mapconcat (lambda (results) (concat "** " (car results) "\n" (string-join (mapcar (lambda (r) (concat "- " r)) (cdr results)) "\n") "\n")) (seq-filter (lambda (result) (not (seq-empty-p (cdr result)))) (mapcar (lambda (saved) (cons (car saved) (mapcar #'my/org-review-format-element (my/org-review-exec-ql saved rev-date)))) my/org-ql-review-queries)) "\n")) #+end_src **** Capture template Now, we have to put all this together and define a capture template for the review. +I'll use a separate directory for the review files, just like for org-journal and org-roam.+ I'll store the review files in org-roam. Time will tell if that's a good idea. The filename will have a format =YYYY-MM-DD.org=, which will also free me from the effort of storing the last review date somewhere. If somehow there are no files in the folder, fallback to the current date minus one two week. Also featuring the most awkward date transformation I've ever done just to add one date. #+begin_src emacs-lisp (setq my/org-review-directory "review") (defun my/get-last-review-date () (-> (substring (or (-max-by 'string-greaterp (-filter (lambda (f) (not (or (string-equal f ".") (string-equal f "..")))) (directory-files (f-join org-roam-directory my/org-review-directory)))) (format-time-string "%Y-%m-%d" (time-subtract (current-time) (seconds-to-time (* 60 60 24 14))))) 0 10) (concat "T00:00:00-00:00") parse-time-string encode-time (time-add (seconds-to-time (* 60 60 24))) ((lambda (time) (format-time-string "%Y-%m-%d" time))))) #+end_src A template looks like this: #+begin_src emacs-lisp (setq my/org-review-capture-template `("r" "Review" plain ,(string-join '("#+title: %<%Y-%m-%d>: REVIEW" "#+category: REVIEW" "#+filetags: log review" "#+STARTUP: overview" "" "Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))" "" "* Roam" "%(my/org-review-format-roam (my/org-changed-files-since-date (my/get-last-review-date)))" "* Agenda" "%(my/org-review-format-queries (my/get-last-review-date))" "* Thoughts" "%?") "\n") :if-new (file "review/%<%Y-%m-%d>.org.gpg"))) (defun my/org-roam-capture-review () (interactive) (org-roam-capture- :node (org-roam-node-create) :templates `(,my/org-review-capture-template))) #+end_src *** org-ref | Type | Description | |------+---------------------------------| | TODO | Figure out how not to load Helm | [[https://github.com/jkitchin/org-ref][org-ref]] is a package that provides support for various citations & references in Org mode. Useful to use BibTeX citations in LaTeX export. As of now, this package loads Helm on start. To avoid this, I have to exclude Helm from the =Package-requires= in the [[file:.emacs.d/straight/repos/org-ref/org-ref.el][org-ref.el]] file. I haven't found a way to do this without modifying the package source yet. #+begin_src emacs-lisp (use-package org-ref :straight (:files (:defaults (:exclude "*helm*"))) :if (not my/remote-server) :init (setq org-ref-completion-library 'org-ref-ivy-cite) (setq bibtex-dialect 'biblatex) (setq org-ref-default-bibliography '("~/Documents/org-mode/bibliography.bib")) (setq reftex-default-bibliography org-ref-default-bibliography) (setq bibtex-completion-bibliography org-ref-default-bibliography) :after (org) :config (general-define-key :keymaps 'org-mode-map "C-c l" #'org-ref-insert-link-hydra/body) (general-define-key :keymaps 'bibtex-mode-map "M-RET" 'org-ref-bibtex-hydra/body) ;; (add-to-list 'orhc-candidate-formats ;; '("online" . " |${=key=}| ${title} ${url}")) ) (defun my/org-ref-select-bibliograhy () (interactive) (setq-local org-ref-default-bibliography `(,(read-file-name "Bibliograhy: " nil nil t))) (setq-local reftex-default-bibliography org-ref-default-bibliography) (setq-local bibtex-completion-bibliography org-ref-default-bibliography)) #+end_src *** org-roam-bibtex Integration with bibtex and org-ref. There are some problems with org roam v2, so I disabled it as of now. I will probably use another way of managing bibliography notes anyway. #+begin_src emacs-lisp (use-package org-roam-bibtex :straight (:host github :repo "org-roam/org-roam-bibtex") :after (org-roam org-ref) :disabled :config (org-roam-bibtex-mode)) #+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 ** UI *** OFF (OFF) Instant equations preview Instant math previews for org mode. References: - [[https://github.com/yangsheng6810/org-latex-impatient][org-latex-impatient repo]] #+begin_src emacs-lisp (use-package org-latex-impatient :straight (:repo "yangsheng6810/org-latex-impatient" :branch "master" :host github) :hook (org-mode . org-latex-impatient-mode) :disabled :init (setq org-latex-impatient-tex2svg-bin "/home/pavel/Programs/miniconda3/lib/node_modules/mathjax-node-cli/bin/tex2svg") (setq org-latex-impatient-scale 1.75) (setq org-latex-impatient-delay 1) (setq org-latex-impatient-border-color "#ffffff")) #+end_src *** 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 :noweb-ref org-ui-setup :tangle no (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 :noweb-ref org-ui-setup :tangle no (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 Remove the ellipsis at the end of folded headlines. The ellipsis 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)) (add-hook 'org-mode-hook #'my/org-no-ellipsis-in-headlines) #+end_src *** OFF (OFF) Org Agenda Icons Categories are broad labels to group agenda items. #+begin_src emacs-lisp :tangle no (if (not my/lowpower) (setq org-agenda-category-icon-alist `(("inbox" ,(list (all-the-icons-faicon "inbox")) nil nil :ascent center) ("work" ,(list (all-the-icons-faicon "cog")) nil nil :ascent center) ("education" ,(list (all-the-icons-material "build")) nil nil :ascent center) ("personal" ,(list (all-the-icons-faicon "music")) nil nil :ascent center) ("misc" ,(list (all-the-icons-material "archive")) nil nil :ascent center) ;; ("lesson" ,(list (all-the-icons-faicon "book")) nil nil :ascent center) ;; ("meeting" ,(list (all-the-icons-material "chat")) nil nil :ascent center) ;; ("event" ,(list (all-the-icons-octicon "clock")) nil nil :ascent center) ("." ,(list (all-the-icons-faicon "circle-o")) nil nil :ascent center)))) #+end_src ** Export *** General settings #+begin_src emacs-lisp ;; (setq org-export-backends '(md html latex beamer org)) #+end_src *** Hugo #+begin_src emacs-lisp (use-package ox-hugo :straight t :after ox) #+end_src *** Jupyter Notebook #+begin_src emacs-lisp (use-package ox-ipynb :straight (:host github :repo "jkitchin/ox-ipynb") :after ox) #+end_src *** Html export #+begin_src emacs-lisp (use-package htmlize :straight t :after ox :config (setq org-html-htmlize-output-type 'css)) #+end_src *** LaTeX Add a custom LaTeX template without default packages. Packages are indented to be imported with function from [[Import *.sty]]. #+begin_src emacs-lisp (defun my/setup-org-latex () (setq org-latex-prefer-user-labels t) (setq org-latex-compiler "xelatex") ;; Probably not necessary (setq org-latex-pdf-process '("latexmk -outdir=%o %f")) ;; Use latexmk (setq org-latex-listings 'minted) ;; Use minted to highlight source code (setq org-latex-minted-options ;; Some minted options I like '(("breaklines" "true") ("tabsize" "4") ("autogobble") ("linenos") ("numbersep" "0.5cm") ("xleftmargin" "1cm") ("frame" "single"))) ;; Use extarticle without the default packages (add-to-list 'org-latex-classes '("org-plain-extarticle" "\\documentclass{extarticle} [NO-DEFAULT-PACKAGES] [PACKAGES] [EXTRA]" ("\\section{%s}" . "\\section*{%s}") ("\\subsection{%s}" . "\\subsection*{%s}") ("\\subsubsection{%s}" . "\\subsubsection*{%s}") ("\\paragraph{%s}" . "\\paragraph*{%s}") ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))) ;; Use beamer without the default packages (add-to-list 'org-latex-classes '("org-latex-beamer" "\\documentclass{beamer} [NO-DEFAULT-PACKAGES] [PACKAGES] [EXTRA]" ("beamer" "\\documentclass[presentation]{beamer}" ("\\section{%s}" . "\\section*{%s}") ("\\subsection{%s}" . "\\subsection*{%s}") ("\\subsubsection{%s}" . "\\subsubsection*{%s}"))))) ;; Make sure to eval the function when org-latex-classes list already exists (with-eval-after-load 'ox-latex (my/setup-org-latex)) #+end_src ** Keybindings & stuff *** General keybindings #+begin_src emacs-lisp :tangle no :noweb-ref org-keys-setup (general-define-key :keymaps 'org-mode-map "C-c d" 'org-decrypt-entry "C-c e" 'org-encrypt-entry "M-p" 'org-latex-preview "M-o" 'org-redisplay-inline-images) (general-define-key :keymaps 'org-mode-map :states '(normal emacs) "L" 'org-shiftright "H" 'org-shiftleft "S-" 'org-next-visible-heading "S-" 'org-previous-visible-heading "M-0" 'org-next-visible-heading "M-9" 'org-previous-visible-heading "M-]" 'org-babel-next-src-block "M-[" 'org-babel-previous-src-block) (general-define-key :keymaps 'org-agenda-mode-map "M-]" 'org-agenda-later "M-[" 'org-agenda-earlier) ;; (general-imap :keymaps 'org-mode-map "RET" 'evil-org-return) (general-nmap :keymaps 'org-mode-map "RET" 'org-ctrl-c-ctrl-c) ;; (my-leader-def "aa" 'org-agenda) #+end_src *** Copy a link #+begin_src emacs-lisp :noweb-ref org-keys-setup (defun my/org-link-copy (&optional arg) "Extract URL from org-mode link and add it to kill ring." (interactive "P") (let* ((link (org-element-lineage (org-element-context) '(link) t)) (type (org-element-property :type link)) (url (org-element-property :path link)) (url (concat type ":" url))) (kill-new url) (message (concat "Copied URL: " url)))) (general-nmap :keymaps 'org-mode-map "C-x C-l" 'my/org-link-copy) #+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* ((default-directory org-directory) (project-files (seq-filter (lambda (f) (and (string-match-p (rx (* nonl) ".org" eos) f) (not (string-match-p (rx (| "journal" "roam" "review" "archive")) f)))) (projectile-current-project-files)))) (find-file (concat org-directory "/" (completing-read "Org file: " project-files))))) (my-leader-def "o o" 'my/org-file-open) #+end_src ** System configuration Functions related to literate configuration. *** Tables for Guix Dependencies A function to extract Guix dependencies from the org file. - If column name matches =[G|g]uix.*dep=, its contents will be added to the result. - If =CATEGORY= is passed, a column with name =[C|c]ategory= will be used to filter results. That way one file can be used to produce multiple manifests. - If =CATEGORY= is not passed, entries with non-empty category will be filtered out - If there is a =[D|d]isabled= column, entries that have a non-empty value in this column will be filtered out. #+begin_src emacs-lisp :noweb-ref guix-tables (defun my/extract-guix-dependencies (&optional category) (let ((dependencies '())) (org-table-map-tables (lambda () (let* ((table (seq-filter (lambda (q) (not (eq q 'hline))) (org-table-to-lisp))) (dep-name-index (cl-position nil (mapcar #'substring-no-properties (nth 0 table)) :test (lambda (_ elem) (string-match-p "[G|g]uix.*dep" elem)))) (category-name-index (cl-position nil (mapcar #'substring-no-properties (nth 0 table)) :test (lambda (_ elem) (string-match-p ".*[C|c]ategory.*" elem)))) (disabled-name-index (cl-position nil (mapcar #'substring-no-properties (nth 0 table)) :test (lambda (_ elem) (string-match-p ".*[D|d]isabled.*" elem))))) (when dep-name-index (dolist (elem (cdr table)) (when (and ;; Category (or ;; Category not set and not present in the table (and (or (not category) (string-empty-p category)) (not category-name-index)) ;; Category is set and present in the table (and category-name-index (not (string-empty-p category)) (string-match-p category (nth category-name-index elem)))) ;; Not disabled (or (not disabled-name-index) (string-empty-p (nth disabled-name-index elem)))) (add-to-list 'dependencies (substring-no-properties (nth dep-name-index elem))))))))) dependencies)) #+end_src Now, join the dependencies list to make it compatible with Scheme: #+begin_src emacs-lisp :noweb-ref guix-tables (defun my/format-guix-dependencies (&optional category) (mapconcat (lambda (e) (concat "\"" e "\"")) (my/extract-guix-dependencies category) "\n")) #+end_src *** Noweb evaluations Turn off eval confirmations for configuration files. #+begin_src emacs-lisp (setq my/org-config-files '("/home/pavel/Emacs.org" "/home/pavel/Desktop.org" "/home/pavel/Console.org" "/home/pavel/Guix.org" "/home/pavel/Mail.org")) (add-hook 'org-mode-hook (lambda () (when (member (buffer-file-name) my/org-config-files) (setq-local org-confirm-babel-evaluate nil)))) #+end_src *** yadm hook A script to run tangle from CLI. #+begin_src emacs-lisp :tangle ~/.config/yadm/hooks/run-tangle.el :noweb yes (require 'org) (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (shell . t))) ;; Do not ask to confirm evaluations (setq org-confirm-babel-evaluate nil) <> ;; A few dummy modes to avoid being prompted for comment systax (define-derived-mode fish-mode prog-mode "Fish" (setq-local comment-start "# ") (setq-local comment-start-skip "#+[\t ]*")) (define-derived-mode yaml-mode text-mode "YAML" (setq-local comment-start "# ") (setq-local comment-start-skip "#+ *")) (mapcar #'org-babel-tangle-file '("/home/pavel/Emacs.org" "/home/pavel/Desktop.org" "/home/pavel/Console.org" "/home/pavel/Guix.org" "/home/pavel/Mail.org")) #+end_src To launch from CLI, run: #+begin_src bash :tangle no emacs -Q --batch -l run-tangle.el #+end_src I have added this line to yadm's =post_alt= hook, so tangle is run after =yadm alt= * Applications ** Dired Dired is a built-in file manager. I currently use it as my primary file manager. *** Basic config & keybindings My config mostly follows ranger's and vifm's keybindings which I'm used to. #+begin_src emacs-lisp (use-package dired :ensure nil :custom ((dired-listing-switches "-alh --group-directories-first")) :commands (dired) :config (setq dired-dwim-target t) (setq wdired-allow-to-change-permissions t) (setq wdired-create-parent-directories t) (setq dired-recursive-copies 'always) (setq dired-recursive-deletes 'always) (setq dired-kill-when-opening-new-dired-buffer t) (add-hook 'dired-mode-hook (lambda () (setq truncate-lines t) (visual-line-mode nil))) (general-define-key :states '(normal) :keymaps 'dired-mode-map "h" 'dired-up-directory "l" 'dired-find-file "=" 'dired-narrow "-" 'dired-create-empty-file "~" 'vterm "" 'dired-up-directory "" 'dired-find-file "M-" 'dired-open-xdg)) (defun my/dired-home () "Open dired at $HOME" (interactive) (dired (expand-file-name "~"))) (my-leader-def "ad" #'dired "aD" (my/command-in-persp "dired $HOME" "dired" nil (dired (expand-file-name "~")))) #+end_src *** Addons I used to use [[https://www.emacswiki.org/emacs/DiredPlus][dired+]], which provides a lot of extensions for dired functionality, but it also creates some new problems, so I opt out of it. Fortunately, the one feature I want from this package - adding more colors to dired buffers - is available as a separate package. #+begin_src emacs-lisp (use-package diredfl :straight t :after (dired) :config (diredfl-global-mode 1)) #+end_src +Reuse the current dired buffer instead of spamming new ones.+ Looks like not necessary with Emacs 28.1 #+begin_src emacs-lisp (use-package dired-single :after dired :disabled :straight t) #+end_src Display icons for files. | Note | Type | |-----------+----------------------------------------------------------| | *ACHTUNG* | This plugin is slow as hell with TRAMP or in =gnu/store= | #+begin_src emacs-lisp (use-package all-the-icons-dired :straight t :if (not (or my/lowpower my/slow-ssh (not (display-graphic-p)))) :hook (dired-mode . (lambda () (unless (string-match-p "/gnu/store" default-directory) (all-the-icons-dired-mode)))) :config (advice-add 'dired-add-entry :around #'all-the-icons-dired--refresh-advice) (advice-add 'dired-remove-entry :around #'all-the-icons-dired--refresh-advice) (advice-add 'dired-kill-subdir :around #'all-the-icons-dired--refresh-advice)) #+end_src Provides stuff like =dired-open-xdg= #+begin_src emacs-lisp (use-package dired-open :straight t :commands (dired-open-xdg)) #+end_src vifm-like filter #+begin_src emacs-lisp (use-package dired-narrow :straight t :commands (dired-narrow) :config (general-define-key :keymaps 'dired-narrow-map [escape] 'keyboard-quit)) #+end_src Display git info, such as the last commit for file and stuff. It's pretty useful but also slows down Dired a bit, hence I don't turn it out by default. #+begin_src emacs-lisp (use-package dired-git-info :straight t :after dired :if (not my/slow-ssh) :config (general-define-key :keymap 'dired-mode-map :states '(normal emacs) ")" 'dired-git-info-mode)) #+end_src *** Subdirectories Subdirectories are one of the interesting features of Dired. It allows displaying multiple folders on the same window. I add my own keybindings and some extra functionality. #+begin_src emacs-lisp (defun my/dired-open-this-subdir () (interactive) (dired (dired-current-directory))) (defun my/dired-kill-all-subdirs () (interactive) (let ((dir dired-directory)) (kill-buffer (current-buffer)) (dired dir))) (with-eval-after-load 'dired (general-define-key :states '(normal) :keymaps 'dired-mode-map "s" nil "ss" 'dired-maybe-insert-subdir "sl" 'dired-maybe-insert-subdir "sq" 'dired-kill-subdir "sk" 'dired-prev-subdir "sj" 'dired-next-subdir "sS" 'my/dired-open-this-subdir "sQ" 'my/dired-kill-all-subdirs (kbd "TAB") 'dired-hide-subdir)) #+end_src *** TRAMP TRAMP is a package that provides remote editing capacities. It is particularly useful for remote server management. One of the reasons why TRAMP may be slow is that some plugins do too many requests to the filesystem. To debug these issues, set the following variable to 6: #+begin_src emacs-lisp (setq tramp-verbose 1) #+end_src To check if a file is remote, you can use ~file-remote-p~. E.g. ~(file-remote-p default-directory)~ for a current buffer. The problem with this approach is that it's rather awkward to add these checks in every hook, especially for global modes, so for now, I just set an environment variable for Emacs which disables these modes. So far I have found the following problematic plugins: | Plugin | Note | Solution | |---------------------+------------------------------------------+-------------------------------| | editorconfig | looks for .editorconfig in the file tree | do not enable globally | | all-the-icons-dired | runs test on every file in the directory | disable | | projectile | looks for .git, .svn, etc | advice ~projectile-file-name~ | | lsp | does a whole lot of stuff | disable | | git-gutter | runs git | disable | | vterm | no proper TRAMP integration | use eshell or shell | At any rate, it's usable, although not perfect. Some other optimization settings: #+begin_src emacs-lisp (setq remote-file-name-inhibit-cache nil) (setq vc-ignore-dir-regexp (format "\\(%s\\)\\|\\(%s\\)" vc-ignore-dir-regexp tramp-file-name-regexp)) #+end_src Set the default shell to =bin/bash= for TRAMP or on a remote server. #+begin_src emacs-lisp (when (or my/remote-server my/slow-ssh) (setq explicit-shell-file-name "/bin/bash")) #+end_src Also, here is a hack to make TRAMP find =ls= on Guix: #+begin_src emacs-lisp (with-eval-after-load 'tramp (setq tramp-remote-path (append tramp-remote-path '(tramp-own-remote-path)))) #+end_src *** Bookmarks A simple bookmark list for Dired, mainly to use with TRAMP. I may look into a proper bookmarking system later. Bookmarks are listed in the [[file:.emacs.d/private.el][private.el]] file, which has an expression like this: #+begin_example emacs-lisp :tangle no (setq my/dired-bookmarks '(("sudo" . "/sudo::/"))) #+end_example The file itself is encrypted with yadm. #+begin_src emacs-lisp (defun my/dired-bookmark-open () (interactive) (let ((bookmarks (mapcar (lambda (el) (cons (format "%-30s %s" (car el) (cdr el)) (cdr el))) my/dired-bookmarks))) (dired (cdr (assoc (completing-read "Dired: " bookmarks nil nil "^") bookmarks))))) #+end_src ** Shells *** vterm My terminal emulator of choice. References: - [[https://github.com/akermu/emacs-libvterm][emacs-libvterm repo]] **** Configuration I use the package from the Guix repository to avoid building libvterm. #+begin_src emacs-lisp (use-package vterm ;; :straight t :commands (vterm vterm-other-window) :config (setq vterm-kill-buffer-on-exit t) (add-hook 'vterm-mode-hook (lambda () (setq-local global-display-line-numbers-mode nil) (display-line-numbers-mode 0))) (advice-add 'evil-collection-vterm-insert :before (lambda (&rest args) (ignore-errors (apply #'vterm-reset-cursor-point args)))) (general-define-key :keymaps 'vterm-mode-map "M-q" 'vterm-send-escape "C-h" 'evil-window-left "C-l" 'evil-window-right "C-k" 'evil-window-up "C-j" 'evil-window-down "C-" 'evil-window-right "C-" 'evil-window-left "C-" 'evil-window-up "C-" 'evil-window-down "M-" 'vterm-send-left "M-" 'vterm-send-right "M-" 'vterm-send-up "M-" 'vterm-send-down) (general-define-key :keymaps 'vterm-mode-map :states '(normal insert) "" 'vterm-beginning-of-line "" 'vterm-end-of-line) (general-define-key :keymaps 'vterm-mode-map :states '(insert) "C-r" 'vterm-send-C-r "C-k" 'vterm-send-C-k "C-j" 'vterm-send-C-j "M-l" 'vterm-send-right "M-h" 'vterm-send-left "M-k" 'vterm-send-up "M-j" 'vterm-send-down)) #+end_src **** Subterminal Open a terminal in the lower third of the frame with the =`= key. #+begin_src emacs-lisp (add-to-list 'display-buffer-alist `(,"vterm-subterminal.*" (display-buffer-reuse-window display-buffer-in-side-window) (side . bottom) (reusable-frames . visible) (window-height . 0.33))) (defun my/toggle-vterm-subteminal () "Toogle subteminal." (interactive) (let ((vterm-window (seq-find (lambda (window) (string-match "vterm-subterminal.*" (buffer-name (window-buffer window)))) (window-list)))) (if vterm-window (if (eq (get-buffer-window (current-buffer)) vterm-window) (kill-buffer (current-buffer)) (select-window vterm-window)) (vterm-other-window "vterm-subterminal")))) (unless my/slow-ssh (general-nmap "`" 'my/toggle-vterm-subteminal) (general-nmap "~" 'vterm)) #+end_src **** Dired integration A function to get pwd for vterm. Couldn't find a built-in function for some reason, but this seems to be working fine: #+begin_src emacs-lisp (defun my/vterm-get-pwd () (if vterm--process (file-truename (format "/proc/%d/cwd" (process-id vterm--process))) default-directory)) #+end_src Now we can open dired for vterm pwd: #+begin_src emacs-lisp (defun my/vterm-dired-other-window () "Open dired in vterm pwd in other window" (interactive) (dired-other-window (my/vterm-get-pwd))) (defun my/vterm-dired-replace () "Replace vterm with dired" (interactive) (let ((pwd (my/vterm-get-pwd))) (kill-process vterm--process) (dired pwd))) #+end_src The second function is particularly handy because that way I can alternate between vterm and dired. Keybindings: #+begin_src emacs-lisp (with-eval-after-load 'vterm (general-define-key :keymaps 'vterm-mode-map :states '(normal) "gd" #'my/vterm-dired-other-window "gD" #'my/vterm-dired-replace)) #+end_src **** With-editor integration A package used by Magit to use the current Emacs instance as the =$EDITOR=. That is, with the help of [[file:Console.org::Functions][this function]], I can just write =e =, edit the file, and then return to the same vterm buffer. No more running vim inside Emacs. #+begin_src emacs-lisp (use-package with-editor :straight t :after (vterm) :config (add-hook 'vterm-mode-hook 'with-editor-export-editor)) #+end_src *** Eshell A shell written in Emacs lisp. I don't use it as of now, but keep the config just in case. #+begin_src emacs-lisp (defun my/configure-eshell () (add-hook 'eshell-pre-command-hook 'eshell-save-some-history) (add-to-list 'eshell-output-filter-functions 'eshell-truncate-buffer) (setq eshell-history-size 10000) (setq eshell-hist-ingnoredups t) (setq eshell-buffer-maximum-lines 10000) (evil-define-key '(normal insert visual) eshell-mode-map (kbd "") 'eshell-bol) (evil-define-key '(normal insert visual) eshell-mode-map (kbd "C-r") 'counsel-esh-history) (general-define-key :states '(normal) :keymaps 'eshell-mode-map (kbd "C-h") 'evil-window-left (kbd "C-l") 'evil-window-right (kbd "C-k") 'evil-window-up (kbd "C-j") 'evil-window-down)) (use-package eshell :ensure nil :after evil-collection :commands (eshell) :config (add-hook 'eshell-first-time-mode-hook 'my/configure-eshell 90) (when my/slow-ssh (add-hook 'eshell-mode-hook (lambda () (setq-local company-idle-delay 1000)))) (setq eshell-banner-message "")) (use-package aweshell :straight (:repo "manateelazycat/aweshell" :host github) :after eshell :config (setq eshell-highlight-prompt nil) (setq eshell-prompt-function 'epe-theme-pipeline)) (use-package eshell-info-banner :defer t :if (not my/slow-ssh) :straight (eshell-info-banner :type git :host github :repo "phundrak/eshell-info-banner.el") :hook (eshell-banner-load . eshell-info-banner-update-banner)) (when my/slow-ssh (general-nmap "`" 'aweshell-dedicated-toggle) (general-nmap "~" 'eshell)) #+end_src ** Managing dotfiles A bunch of functions for managing dotfiles with yadm. *** Open Emacs config #+begin_src emacs-lisp (general-define-key ;; "C-c c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org")) "C-c c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org")) (my-leader-def :infix "c" "" '(:which-key "configuration") ;; "c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org")) "c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org")) #+end_src *** Open Magit for yadm Idea: - [[https://www.reddit.com/r/emacs/comments/gjukb3/yadm_magit/]] #+begin_src emacs-lisp (with-eval-after-load 'tramp (add-to-list 'tramp-methods `("yadm" (tramp-login-program "yadm") (tramp-login-args (("enter"))) (tramp-login-env (("SHELL") "/bin/sh")) (tramp-remote-shell "/bin/sh") (tramp-remote-shell-args ("-c"))))) (defun my/yadm-magit () (interactive) (magit-status "/yadm::")) (my-leader-def "cm" 'my/yadm-magit) #+end_src *** Open a dotfile Open a file managed by yadm. #+begin_src emacs-lisp (defun my/open-yadm-file () "Open a file managed by yadm" (interactive) (find-file (concat (file-name-as-directory (getenv "HOME")) (completing-read "yadm files: " (split-string (shell-command-to-string "yadm ls-files $HOME --full-name") "\n"))))) (general-define-key "C-c f" '(my/open-yadm-file :wk "yadm file")) (my-leader-def "cf" '(my/open-yadm-file :wk "yadm file")) #+end_src ** Internet & Multimedia *** Notmuch My notmuch config now resides in [[file:Mail.org][Mail.org]]. #+begin_src emacs-lisp (unless (or my/is-termux my/remote-server) (load-file (expand-file-name "mail.el" user-emacs-directory))) #+end_src *** Elfeed [[https://github.com/skeeto/elfeed][elfeed]] is an Emacs RSS client. The advice there sets =shr-use-fonts= to nil while rendering HTML, so the =elfeed-show= buffer will use monospace font. Using my own fork until the modifications are merged into master. #+begin_src emacs-lisp (use-package elfeed :straight (:repo "SqrtMinusOne/elfeed" :host github) :if (not my/remote-server) :commands (elfeed) :init (my-leader-def "ae" (my/command-in-persp "elfeed" "elfeed" 0 (elfeed))) :config (setq elfeed-db-directory "~/.elfeed") (setq elfeed-enclosure-default-dir (expand-file-name "~/Downloads")) (advice-add #'elfeed-insert-html :around (lambda (fun &rest r) (let ((shr-use-fonts nil)) (apply fun r)))) (general-define-key :states '(normal) :keymaps 'elfeed-search-mode-map "o" #'my/elfeed-search-filter-source "c" #'elfeed-search-clear-filter "gl" (lambda () (interactive) (elfeed-search-set-filter "+later"))) (general-define-key :states '(normal) :keymaps 'elfeed-show-mode-map "ge" #'my/elfeed-show-visit-eww)) #+end_src [[https://github.com/remyhonig/elfeed-org][elfeed-org]] allows configuring Elfeed feeds with an Org file. #+begin_src emacs-lisp (use-package elfeed-org :straight t :after (elfeed) :config (setq rmh-elfeed-org-files '("~/.emacs.d/private.org")) (elfeed-org)) #+end_src **** Some additions Filter elfeed search buffer by the feed under the cursor. #+begin_src emacs-lisp (defun my/elfeed-search-filter-source (entry) "Filter elfeed search buffer by the feed under cursor." (interactive (list (elfeed-search-selected :ignore-region))) (when (elfeed-entry-p entry) (elfeed-search-set-filter (concat "@6-months-ago " "+unread " "=" (replace-regexp-in-string (rx "?" (* not-newline) eos) "" (elfeed-feed-url (elfeed-entry-feed entry))))))) #+end_src Open a URL with eww. #+begin_src emacs-lisp (defun my/elfeed-show-visit-eww () "Visit the current entry in eww" (interactive) (let ((link (elfeed-entry-link elfeed-show-entry))) (when link (eww link)))) #+end_src **** Custom faces Setting up custom faces for certain tags to make the feed look a bit nicer. #+begin_src emacs-lisp (defface elfeed-videos-entry `((t :foreground ,(doom-color 'red))) "Face for the elfeed entries with tag \"videos\"") (defface elfeed-twitter-entry `((t :foreground ,(doom-color 'blue))) "Face for the elfeed entries with tah \"twitter\"") (defface elfeed-emacs-entry `((t :foreground ,(doom-color 'magenta))) "Face for the elfeed entries with tah \"emacs\"") (defface elfeed-music-entry `((t :foreground ,(doom-color 'green))) "Face for the elfeed entries with tah \"music\"") (defface elfeed-podcasts-entry `((t :foreground ,(doom-color 'yellow))) "Face for the elfeed entries with tag \"podcasts\"") (defface elfeed-blogs-entry `((t :foreground ,(doom-color 'orange))) "Face for the elfeed entries with tag \"blogs\"") (with-eval-after-load 'elfeed (setq elfeed-search-face-alist '((twitter elfeed-twitter-entry) (podcasts elfeed-podcasts-entry) (music elfeed-music-entry) (videos elfeed-videos-entry) (emacs elfeed-emacs-entry) (blogs elfeed-blogs-entry) (unread elfeed-search-unread-title-face)))) #+end_src Also, a function to automatically adjust these colors with the Doom theme. #+begin_src emacs-lisp (defun my/update-my-theme-elfeed (&rest _) (custom-theme-set-faces 'my-theme-1 `(elfeed-videos-entry ((t :foreground ,(doom-color 'red)))) `(elfeed-twitter-entry ((t :foreground ,(doom-color 'blue)))) `(elfeed-emacs-entry ((t :foreground ,(doom-color 'magenta)))) `(elfeed-music-entry ((t :foreground ,(doom-color 'green)))) `(elfeed-podcasts-entry ((t :foreground ,(doom-color 'yellow)))) `(elfeed-blogs-entry ((t :foreground ,(doom-color 'orange))))) (enable-theme 'my-theme-1)) (advice-add 'load-theme :after #'my/update-my-theme-elfeed) (when (fboundp 'doom-color) (my/update-my-theme-elfeed)) #+end_src **** elfeed-score [[https://github.com/sp1ff/elfeed-score][elfeed-score]] is a package that implements scoring for the elfeed entries. Entries are scored by a set of rules for tags/title/content/etc and sorted by that score. #+begin_src emacs-lisp (defun my/elfeed-toggle-score-sort () (interactive) (setq elfeed-search-sort-function (if elfeed-search-sort-function nil #'elfeed-score-sort)) (message "Sorting by score: %S" (if elfeed-search-sort-function "ON" "OFF")) (elfeed-search-update--force)) (use-package elfeed-score :straight t :after (elfeed) :init (setq elfeed-score-serde-score-file "~/.emacs.d/elfeed.score") :config (elfeed-score-enable) (setq elfeed-search-print-entry-function #'elfeed-score-print-entry) (general-define-key :states '(normal) :keymaps '(elfeed-search-mode-map) "=" elfeed-score-map) (general-define-key :keymaps '(elfeed-score-map) "=" #'my/elfeed-toggle-score-sort)) #+end_src **** YouTube, podcasts & EMMS Previously this block was opening MPV with =start-process=, but now I've managed to hook up MPV with EMMS. So there is the EMMS+elfeed "integration". There are multiple kinds of entries that I want to be opened by EMMS. First, a function that returns a YouTube URL: #+begin_src emacs-lisp (defun my/get-youtube-url (entry) (let ((watch-id (cadr (assoc "watch?v" (url-parse-query-string (substring (url-filename (url-generic-parse-url (elfeed-entry-link entry))) 1)))))) (when watch-id (concat "https://www.youtube.com/watch?v=" watch-id)))) #+end_src Second, a function that returns a URL to an enclosure. This is generally how podcasts are distributed. #+begin_src emacs-lisp (defun my/get-enclosures-url (entry) (caar (elfeed-entry-enclosures entry))) #+end_src Now, a function to add a YouTube link with metadata from elfeed to EMMS. #+begin_src emacs-lisp (with-eval-after-load 'emms (define-emms-source elfeed (entry) (let ((url (or (my/get-enclosures-url entry) (my/get-youtube-url entry)))) (unless url (error "URL not found")) (let ((track (emms-track 'url url))) (emms-track-set track 'info-title (elfeed-entry-title entry)) (emms-playlist-insert-track track))))) (defun my/elfeed-add-emms () (interactive) (emms-add-elfeed elfeed-show-entry) (elfeed-tag elfeed-show-entry 'watched) (elfeed-show-refresh)) (with-eval-after-load 'elfeed (general-define-key :states '(normal) :keymaps 'elfeed-show-mode-map "gm" #'my/elfeed-add-emms)) #+end_src *** EMMS EMMS is the Emacs Multi-Media System. I use it to control MPD & MPV. References: - [[https://www.gnu.org/software/emms/manual/][EMMS Manual]] - [[https://www.youtube.com/watch?v=xTVN8UDScqk][Uncle Dave's video]] #+begin_src emacs-lisp :noweb yes (use-package emms :straight t :if (not my/remote-server) :commands (emms-smart-browse emms-browser emms-add-url emms-add-file emms-add-find) :if (not my/is-termux) :init (my-leader-def :infix "as" "" '(:which-key "emms") "s" (my/command-in-persp "emms" "EMMS" 0 (emms-smart-browse)) "b" 'emms-browser "p" 'emms-pause "q" 'emms-stop "h" 'emms-previous "l" 'emms-next "u" 'emms-player-mpd-connect) (setq emms-mode-line-icon-enabled-p nil) :config (require 'emms-setup) (require 'emms-player-mpd) (require 'emms-player-mpv) (emms-all) ;; MPD setup <> ;; MPV setup <> ;; evil-lion and evil-commentary shadow some gX bindings ;; (add-hook 'emms-browser-mode-hook ;; (lambda () ;; (evil-lion-mode -1) ;; (evil-commentary-mode -1) ;; )) ;; I have everything I need in polybar (emms-mode-line-mode -1) (emms-playing-time-display-mode -1) <>) #+end_src **** MPD :PROPERTIES: :header-args:emacs-lisp: :tangle no :noweb-ref emms-mpd-setup :END: [[https://www.musicpd.org/][mpd]] is a server for playing music. It has a couple of first-class clients, including curses-based [[https://github.com/ncmpcpp/ncmpcpp][ncmpcpp]], but of course, I want to use Emacs. #+begin_src emacs-lisp (setq emms-source-file-default-directory (expand-file-name "~/Music/")) (add-to-list 'emms-info-functions 'emms-info-mpd) (add-to-list 'emms-player-list 'emms-player-mpd) (setq emms-player-mpd-server-name "localhost") (setq emms-player-mpd-server-port "6600") (setq emms-player-mpd-music-directory "~/Music") #+end_src Connect on setup. For some reason, it stops the mpd playback whenever it connects, but it is not a big issue. #+begin_src emacs-lisp (emms-player-mpd-connect) #+end_src Clear MPD playlist on clearing EMMS playlist. IDK if this is fine for MPD library playlist, I don't use them anyhow. #+begin_src emacs-lisp (add-hook 'emms-playlist-cleared-hook 'emms-player-mpd-clear) #+end_src Set a custom regex for MPD. EMMS sets up the default one from MPD's diagnostic output so that regex opens basically everything, including videos, https links, etc. That is fine if MPD is the only player in EMMS, but as I want to use MPV as well, I override the regex. #+begin_src emacs-lisp (emms-player-set emms-player-mpd 'regex (rx (or (: "https://" (* nonl) (or "acast.com") (* nonl)) (+ (? (or "https://" "http://")) (* nonl) (regexp (eval (emms-player-simple-regexp "m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff" "m4a"))))))) #+end_src After all this is done, run =M-x emms-cache-set-from-mpd-all= to set cache from MPD. If everything is correct, EMMS browser will be populated with MPD database. **** MPV :PROPERTIES: :header-args:emacs-lisp: :tangle no :noweb-ref emms-mpv-setup :END: | Guix dependency | |-----------------| | mpv | | yt-dlp | [[https://mpv.io/][mpv]] is a decent media player, which has found a place in this configuration because it integrates with +youtube-dl+ yt-dlp. It looks like YouTube has started to throttle youtube-dl, and yt-dlp has a workaround for that particular case. Just don't forget to add the following like to the mpv config: #+begin_src conf-unix :tangle ~/.config/mpv/mpv.conf script-opts=ytdl_hook-ytdl_path=yt-dlp #+end_src It seems a bit strange to keep the MPV config in this file, but I don't use the program outside Emacs. #+begin_src emacs-lisp (add-to-list 'emms-player-list 'emms-player-mpv) #+end_src Also a custom regex. My demands for MPV include running =yt-dlp=, so there is a regex that matches youtube.com or some of the video formats. #+begin_src emacs-lisp (emms-player-set emms-player-mpv 'regex (rx (or (: "https://" (* nonl) "youtube.com" (* nonl)) (+ (? (or "https://" "http://")) (* nonl) (regexp (eval (emms-player-simple-regexp "mp4" "mov" "wmv" "webm" "flv" "avi" "mkv"))))))) #+end_src By default, MPV plays the video in the best possible quality, which may be pretty high, even too high with limited bandwidth. So here is the logic to choose the quality. #+begin_src emacs-lisp (setq my/youtube-dl-quality-list '("bestvideo[height<=720]+bestaudio/best[height<=720]" "bestvideo[height<=480]+bestaudio/best[height<=480]" "bestvideo[height<=1080]+bestaudio/best[height<=1080]")) (setq my/default-emms-player-mpv-parameters '("--quiet" "--really-quiet" "--no-audio-display")) (defun my/set-emms-mpd-youtube-quality (quality) (interactive "P") (unless quality (setq quality (completing-read "Quality: " my/youtube-dl-quality-list nil t))) (setq emms-player-mpv-parameters `(,@my/default-emms-player-mpv-parameters ,(format "--ytdl-format=%s" quality)))) (my/set-emms-mpd-youtube-quality (car my/youtube-dl-quality-list)) #+end_src Now =emms-add-url= should work on YouTube URLs just fine. Just keep in mind that it will only add the URL to the playlist, not play it right away. **** Cache cleanup All the added URLs reside in the EMMS cache after being played. I don't want them to stay there for a long time, so here is a handy function to clean it. #+begin_src emacs-lisp (defun my/emms-cleanup-urls () (interactive) (let ((keys-to-delete '())) (maphash (lambda (key value) (when (eq (cdr (assoc 'type value)) 'url) (add-to-list 'keys-to-delete key))) emms-cache-db) (dolist (key keys-to-delete) (remhash key emms-cache-db))) (setq emms-cache-dirty t)) (my-leader-def "asc" #'my/emms-cleanup-urls) #+end_src **** Fetching lyrics My package for fetching EMMS lyrics and album covers. #+begin_src emacs-lisp (use-package lyrics-fetcher :straight t :after (emms) :init (my-leader-def "ast" #'lyrics-fetcher-show-lyrics "asT" #'lyrics-fetcher-show-lyrics-query) :config (setq lyrics-fetcher-genius-access-token (password-store-get "My_Online/APIs/genius.com")) (general-define-key :states '(emacs normal) :keymaps 'emms-browser-mode-map "gl" 'lyrics-fetcher-emms-browser-show-at-point "gC" 'lyrics-fetcher-emms-browser-fetch-covers-at-point "go" 'lyrics-fetcher-emms-browser-open-large-cover-at-point)) #+end_src **** Some keybindings #+begin_src emacs-lisp (with-eval-after-load 'emms-browser (general-define-key :states '(normal) :keymaps 'emms-browser-mode-map "q" 'quit-window)) (with-eval-after-load 'emms (general-define-key :states '(normal) :keymaps 'emms-playlist-mode-map "q" 'quit-window)) #+end_src **** EMMS & mpd Fixes +Some fixes until I submit a patch.+ I've submitted a patch for with these fixes, so I'll remove this section eventually. For some reason EMMS doesn't fetch =albumartist= from MPD. Overriding this function fixes that. #+begin_src emacs-lisp :tangle no :noweb-ref emms-fixes (defun emms-info-mpd-process (track info) (dolist (data info) (let ((name (car data)) (value (cdr data))) (setq name (cond ((string= name "artist") 'info-artist) ((string= name "albumartist") 'info-albumartist) ((string= name "composer") 'info-composer) ((string= name "performer") 'info-performer) ((string= name "title") 'info-title) ((string= name "album") 'info-album) ((string= name "track") 'info-tracknumber) ((string= name "disc") 'info-discnumber) ((string= name "date") 'info-year) ((string= name "genre") 'info-genre) ((string= name "time") (setq value (string-to-number value)) 'info-playing-time) (t nil))) (when name (emms-track-set track name value))))) #+end_src Also, =emms-player-mpd-get-alists= has an interesting bug. This function parses the response to =listallinfo=, which looks something like this: #+begin_example tag1: value1 tag2: value2 ... tag1: value1' tag2: value2' #+end_example This structure has to be converted to list of alists, which looks like: #+begin_example (("tag1" . "value1" "tag2" . "value2") ("tag1" . "value1'" ("tag2" . "value2'"))) #+end_example The original implementation creates a new alist whenever it encounters a tag it has already put in the current alist. Which doesn't work too well if some tags don't repeat, if the order is messed up, etc. Fortunately, according to the [[https://mpd.readthedocs.io/en/latest/protocol.html#command-lsinfo][protocol specification]], each new record has to start with =file=, =directory= or =playlist=. I've overridden the function with that in mind and it fixed the import, at least in my case. #+begin_src emacs-lisp :tangle no :noweb-ref emms-fixes (defun emms-player-mpd-get-alists (info) "Turn the given parsed INFO from MusicPD into an list of alists. The list will be in reverse order." (when (and info (null (car info)) ; no error has occurred (cdr info)) ; data exists (let ((alists nil) (alist nil) cell) (dolist (line (cdr info)) (when (setq cell (emms-player-mpd-parse-line line)) (if (member (car cell) '("file" "directory" "playlist")) (setq alists (cons alist alists) alist (list cell)) (setq alist (cons cell alist))))) (when alist (setq alists (cons alist alists))) alists))) #+end_src *** ytel [[https://github.com/gRastello/ytel][ytel]] is a YouTube (actually Invidious) frontend, which lets one search YouTube (whereas the setup with elfeed just lets one view the pre-defined subscriptions). The package doesn't provide evil bindings, so I define my own. #+begin_src emacs-lisp (use-package ytel :straight t :commands (ytel) :config (setq ytel-invidious-api-url "https://invidio.xamh.de/") (general-define-key :states '(normal) :keymaps 'ytel-mode-map "q" #'ytel-quit "s" #'ytel-search "L" #'ytel-search-next-page "H" #'ytel-search-previous-page "RET" #'my/ytel-add-emms)) #+end_src And here is the same kind of integration with EMMS as in the elfeed setup: #+begin_src emacs-lisp (with-eval-after-load 'emms (define-emms-source ytel (video) (let ((track (emms-track 'url (concat "https://www.youtube.com/watch?v=" (ytel-video-id video))))) (emms-track-set track 'info-title (ytel-video-title video)) (emms-track-set track 'info-artist (ytel-video-author video)) (emms-playlist-insert-track track)))) (defun my/ytel-add-emms () (interactive) (emms-add-ytel (ytel-get-current-video))) #+end_src *** EWW Emacs built-in web browser. +I wonder if anyone actually uses it.+ I use it occasionally to open links in elfeed. #+begin_src emacs-lisp (defun my/toggle-shr-use-fonts () "Toggle the shr-use-fonts variable in buffer" (interactive) (setq-local shr-use-fonts (not shr-use-fonts))) (my-leader-def "aw" 'eww) (general-define-key :keymaps 'eww-mode-map "+" 'text-scale-increase "-" 'text-scale-decrease) #+end_src *** ERC ERC is a built-it Emacs IRC client. #+begin_src emacs-lisp (use-package erc :commands (erc erc-tls) :straight (:type built-in) :init (my-leader-def "ai" (my/command-in-persp "erc" "ERC" 0 (erc-tls))) :config ;; Logging (setq erc-log-channels-directory "~/.erc/logs") (setq erc-save-buffer-on-part t) ;; Config of my ZNC instance. (setq erc-server "sqrtminusone.xyz") (setq erc-port 6697) (setq erc-nick "sqrtminusone") (setq erc-user-full-name "Pavel Korytov") (setq erc-password (password-store-get "Selfhosted/ZNC")) (setq erc-kill-buffer-on-part t) (setq erc-track-shorten-start 8)) #+end_src Exclude everything but actual messages from notifications. #+begin_src emacs-lisp (setq erc-track-exclude-types '("NICK" "JOIN" "LEAVE" "QUIT" "PART" "301" ; away notice "305" ; return from awayness "306" ; set awayness "324" ; modes "329" ; channel creation date "332" ; topic notice "333" ; who set the topic "353" ; Names notice )) #+end_src A plugin to highlight IRC nicknames: #+begin_src emacs-lisp (use-package erc-hl-nicks :hook (erc-mode . erc-hl-nicks-mode) :after (erc) :straight t) #+end_src ZNC support. Seems to provide a few nice features for ZNC. #+begin_src emacs-lisp (use-package znc :straight t :after (erc)) #+end_src *** Google Translate Emacs interface to Google Translate. Can't make it load lazily for some strange reason. References: - [[https://github.com/atykhonov/google-translate][google-translate repo]] - [[https://github.com/atykhonov/google-translate/issues/137#issuecomment-728278849][issue with ttk error fix]] #+begin_src emacs-lisp (use-package google-translate :straight t :functions (my-google-translate-at-point google-translate--search-tkk) :custom (google-translate-backend-method 'curl) :config (require 'facemenu) (defun google-translate--search-tkk () "Search TKK." (list 430675 2721866130)) (defun my-google-translate-at-point() "reverse translate if prefix" (interactive) (if current-prefix-arg (google-translate-at-point) (google-translate-at-point-reverse))) (setq google-translate-translation-directions-alist '(("en" . "ru") ("ru" . "en")))) (my-leader-def :infix "at" "" '(:which-key "google translate") "p" 'google-translate-at-point "P" 'google-translate-at-point-reverse "q" 'google-translate-query-translate "Q" 'google-translate-query-translate-reverse "t" 'google-translate-smooth-translate) #+end_src ** Reading documentation *** tldr [[https://tldr.sh/][tldr]] is a collaborative project providing cheatsheets for various console commands. For some reason, the built-in download in the package is broken, so I use my own function. #+begin_src emacs-lisp (use-package tldr :straight t :commands (tldr) :config (setq tldr-source-zip-url "https://github.com/tldr-pages/tldr/archive/refs/heads/main.zip") (defun tldr-update-docs () (interactive) (shell-command-to-string (format "curl -L %s --output %s" tldr-source-zip-url tldr-saved-zip-path)) (when (file-exists-p "/tmp/tldr") (delete-directory "/tmp/tldr" t)) (shell-command-to-string (format "unzip -d /tmp/tldr/ %s" tldr-saved-zip-path)) (when (file-exists-p tldr-directory-path) (delete-directory tldr-directory-path 'recursive 'no-trash)) (shell-command-to-string (format "mv %s %s" "/tmp/tldr/tldr-main" tldr-directory-path)))) (my-leader-def "hT" 'tldr) #+end_src *** man & info Of course, Emacs can also display man and info pages. #+begin_src emacs-lisp (setq Man-width-max 180) (my-leader-def "hM" 'man) (general-define-key :states '(normal) :keymaps 'Info-mode-map (kbd "RET") 'Info-follow-nearest-node) (defun my/man-fix-width (&rest _) (setq-local Man-width (- (window-width) 4))) (advice-add #'Man-update-manpage :before #'my/man-fix-width) #+end_src *** devdocs.io Finally, there is also an Emacs plugin for [[https://devdocs.io][devdocs.io]]. #+begin_src emacs-lisp (use-package devdocs :straight t :commands (devdocs-install devdocs-lookup) :init (my-leader-def "he" #'devdocs-lookup "hE" #'devdocs-install)) #+end_src ** Utilities *** pass I use [[https://www.passwordstore.org/][pass]] as my password manager. Expectedly, there is Emacs frontend for it. Although I use [[https://github.com/carnager/rofi-pass][this rofi frontend]] for actually inserting passwords. #+begin_src emacs-lisp (use-package pass :straight t :commands (pass) :init (my-leader-def "ak" #'pass) :config (setq pass-show-keybindings nil)) #+end_src *** Docker A package to manage docker containers from Emacs. The file =progidy-config.el= sets variable =my/docker-directories=, which allows to #+begin_src emacs-lisp (use-package docker :straight t :commands (docker) :init (my-leader-def "ao" 'docker)) #+end_src By default, docker commands are run in =default-directory=. Even worse, transient doesn't allow to set =default-directory= temporarily, via =let=. But often I don't want to change =default-directory= of a buffer (e.g. via Dired) to run a command from there. So I decided to implement the following advice: #+begin_src emacs-lisp (setq my/selected-docker-directory nil) (defun my/docker-override-dir (fun &rest args) (let ((default-directory (or my/selected-docker-directory default-directory))) (setq my/selected-docker-directory nil) (apply fun args))) #+end_src It overrides =default-directory= for the first launch of a function. Now, add the advice to the required functions from =docker.el=: #+begin_src emacs-lisp (with-eval-after-load 'docker (advice-add #'docker-compose-run-docker-compose-async :around #'my/docker-override-dir) (advice-add #'docker-compose-run-docker-compose :around #'my/docker-override-dir) (advice-add #'docker-run-docker-async :around #'my/docker-override-dir) (advice-add #'docker-run-docker :around #'my/docker-override-dir)) #+end_src And here is a function which prompts the user for the directory. File =progidy-config.el= sets an alist of possible directories, look the section about [[*Progidy][progidy]]. #+begin_src emacs-lisp (defun my/docker-from-dir () (interactive) (when (not (boundp 'my/docker-directories)) (load (concat user-emacs-directory "prodigy-config"))) (let* ((directories (mapcar (lambda (el) (cons (format "%-30s %s" (car el) (cdr el)) (cdr el))) my/docker-directories)) (selected-directory (cdr (assoc (completing-read "Docker: " directories nil nil "^") directories)))) (setq my/selected-docker-directory selected-directory) (docker))) (my-leader-def "aO" 'my/docker-from-dir) #+end_src *** Progidy [[https://github.com/rejeep/prodigy.el][prodigy.el]] is a package to run various services. I've previously used tmuxp + tmux, but want to try this as well. The actual service definitions are in the =~/.emacs.d/prodigy.org=, which tangles to =prodigy-config.el=. Both files are encrypted in yadm, as they contain personal data. #+begin_src emacs-lisp (use-package prodigy :straight t :commands (prodigy) :init (my-leader-def "aP" 'prodigy) :config (when (not (boundp 'my/docker-directories)) (load (concat user-emacs-directory "prodigy-config"))) (general-define-key :states '(normal) :keymaps 'prodigy-view-mode-map "C-h" 'evil-window-left "C-l" 'evil-window-right "C-k" 'evil-window-up "C-j" 'evil-window-down)) #+end_src A few functions to work with apps on ports. #+begin_src emacs-lisp (defun my/get-apps-on-ports () (mapcar (lambda (line) (let* ((split (split-string line (rx (| (+ " ") (+ "\t"))))) (process (elt split 6))) `((netid . ,(elt split 0)) (state . ,(elt split 1)) (recv-q . ,(elt split 2)) (send-q . ,(elt split 3)) ,@(let ((data (elt split 4))) (save-match-data (string-match (rx (group-n 1 (* nonl)) ":" (group-n 2 (or (+ num) "*"))) data) `((local-address . ,(match-string 1 data)) (local-port . ,(match-string 2 data))))) ,@(unless (string-empty-p process) `((pid . ,(save-match-data (string-match (rx "pid=" (+ num)) process) (string-to-number (substring (match-string 0 process) 4))))))))) (seq-filter (lambda (s) (not (string-empty-p s))) (split-string (shell-command-to-string "ss -tulpnH | grep LISTEN") "\n")))) (defun my/kill-app-on-port (port &optional signal) (let ((apps (my/get-apps-on-ports))) (dolist (app apps) (when (string-equal (cdr (assoc 'local-port app)) port) (signal-process (cdr (assoc 'pid app)) (or signal 15)) (message "Sent %d to %d" (or signal 15) (cdr (assoc 'pid app))))))) #+end_src *** screenshot.el Tecosaur's plugin to make beautiful code screenshots. | Guix dependency | |-----------------| | imagemagick | #+begin_src emacs-lisp (use-package screenshot :straight (:repo "tecosaur/screenshot" :host github :build (:not compile)) :if (display-graphic-p) :commands (screenshot) :init (my-leader-def "S" 'screenshot)) #+end_src *** proced proced is an Emacs built-it process viewer, like top. #+begin_src emacs-lisp (my-leader-def "ah" 'proced) (setq proced-auto-update-interval 1) (add-hook 'proced-mode-hook (lambda () (visual-line-mode -1) (setq-local truncate-lines t) (proced-toggle-auto-update 1))) #+end_src *** Guix An Emacs package to help managing GNU Guix. #+begin_src emacs-lisp (use-package guix :straight t :commands (guix) :init (my-leader-def "ag" 'guix)) #+end_src ** Productivity *** pomm My package for doing Pomodoro timer. #+begin_src emacs-lisp (use-package pomm :straight (:host github :repo "SqrtMinusOne/pomm.el" :files (:defaults "resources")) ;; :straight (:local-repo "~/Code/Emacs/pomm" :files (:defaults "resources")) :commands (pomm) :init (my-leader-def "ap" #'pomm) :config (setq alert-default-style 'libnotify) (add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string) (add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string)) #+end_src *** hledger [[hledger.org/][hledger]] is a plain-text double-entry accounting software. I use it for managing my personal finances, and thus far it's great. | Guix dependency | |-----------------| | hledger | #+begin_src emacs-lisp (use-package hledger-mode :straight t :mode (rx ".journal" eos) :config (setq hledger-jfile (concat org-directory "/ledger/ledger.journal")) (add-hook 'hledger-mode-hook (lambda () (make-local-variable 'company-backends) (add-to-list 'company-backends 'hledger-company)))) (use-package flycheck-hledger :straight t :after (hledger-mode)) #+end_src *** Calendar Emacs' built-in calendar. Can even calculate sunrise and sunset times. #+begin_src emacs-lisp (setq calendar-date-style 'iso) ;; YYYY/mm/dd (setq calendar-week-start-day 1) (setq calendar-time-display-form '(24-hours ":" minutes)) (setq calendar-latitude 59.9375) (setq calendar-longitude 30.308611) #+end_src ** Fun *** Discord integration Integration with Discord. Shows which file is being edited in Emacs. In order for this to work in Guix, a service is necessary - [[file:Desktop.org::*Discord rich presence][Discord rich presence]]. Some functions to override the displayed message: #+begin_src emacs-lisp (defun my/elcord-mask-buffer-name (name) (cond ((string-match-p (rx bos (? "CAPTURE-") (= 14 num) "-" (* not-newline) ".org" eos) name) "") ((string-match-p (rx bos (+ num) "-" (+ num) "-" (+ num) ".org" eos) name) "") ((string-match-p (rx bos "EXWM") name) "") (t name))) (defun my/elcord-buffer-details-format-functions () (format "Editing %s" (my/elcord-mask-buffer-name (buffer-name)))) (defun my/elcord-update-presence-mask-advice (r) (list (my/elcord-mask-buffer-name (nth 0 r)) (nth 1 r))) #+end_src And the package configuration: #+begin_src emacs-lisp (use-package elcord :straight t :if (and (or (string= (system-name) "indigo") (string= (system-name) "eminence")) (not my/slow-ssh) (not my/remote-server)) :config (setq elcord-buffer-details-format-function #'my/elcord-buffer-details-format-functions) (advice-add 'elcord--try-update-presence :filter-args #'my/elcord-update-presence-mask-advice) (elcord-mode)) #+end_src *** Snow #+begin_src emacs-lisp (use-package snow :straight (:repo "alphapapa/snow.el" :host github) :commands (snow)) #+end_src *** Power mode When Emacs doesn't feel powerful enough. Watch out if you are using EXWM. #+begin_src emacs-lisp (use-package power-mode :straight (:host github :repo "elizagamedev/power-mode.el") :disabled :commands (power-mode)) #+end_src *** Redacted #+begin_src emacs-lisp (use-package redacted :commands (redacted-mode) :straight (:host github :repo "bkaestner/redacted.el")) #+end_src *** Zone #+begin_src emacs-lisp (use-package zone :ensure nil :config (setq original-zone-programs (copy-sequence zone-programs))) (defun my/zone-with-select () (interactive) (ivy-read "Zone programs" (cl-pairlis (cl-mapcar 'symbol-name original-zone-programs) original-zone-programs) :action (lambda (elem) (setq zone-programs (vector (cdr elem))) (zone)))) #+end_src Also, a function to copy a URL to the video under cursor. #+begin_src emacs-lisp (defun my/ytel-kill-url () (interactive) (kill-new (concat "https://www.youtube.com/watch?v=" (ytel-video-id (ytel-get-current-video))))) #+end_src * Guix settings | Guix dependency | Description | |---------------------+-------------------------------| | emacs-vterm | A vterm package | | ripgrep | A recursive search tool | | the-silver-searcher | Another recursive search tool | #+NAME: packages #+begin_src emacs-lisp :tangle no (my/format-guix-dependencies) #+end_src #+begin_src scheme :tangle .config/guix/manifests/emacs.scm :noweb yes (specifications->manifest '("emacs-native-comp" <>)) #+end_src