Skip to content

Latest commit

 

History

History
8479 lines (6646 loc) · 359 KB

README.org

File metadata and controls

8479 lines (6646 loc) · 359 KB

Emacs 🦬 Configuration

This is an emoji-heavy 😅 literal[fn:1] configuration for Emacs which is definitely not as feature complete like some distros out there (think: Doom Emacs or Spacemacs) but may still provide you a pragmatic and easily parsable configuration that sticks to standard tooling 🧰 and patterns while providing sufficient context as to what is happening such that beginners[fn:2] can also have a good time cooking 👨🏿‍🔬 up or tweaking 👨🏿‍🔧 their configurations.

See Awesome Emacs for ideas of things to do in your own configuration (in case you aren’t declaring bankruptcy yet).

Installation

I manage both my Emacs installation and my Emacs configuration declaratively. A home-manager configuration written in Nix manages the version and bundle of Emacs on my system while classic Emacs LISP manages the Emacs configuration itself.

⚠️ In case you don’t use Nix ❄️ or can’t be bothered to dive into it (in the same way I can’t be bothered to remember the different build instructions for the tools that I depend on), please skip the part where I install Emacs with home-manager and focus on the manual installation instructions instead.

Developer Notes

Tangle Helper

The my-dotfiles-tangle-wrapper helper allows us to spawn an Emacs session with our freshly tangled configuration in order to spot early errors.

images/screenshot-tangle-helper-full.png

(defun my-dotfiles-tangle-wrapper (orig-fun &rest args)
  (message "Wrapping %S with %S" orig-fun args)
  (let ((res (apply orig-fun args))
        (command "emacs")
        (args (list "-q"
                    "--load=init.el"
                    "--debug-init"
                    "--name=Emacs Test"
                    (format "--file=%s" buffer-file-name))))
    (apply #'start-process `("emacs-for-org-cite-export" nil ,command ,@args))
    (message "Done and got %S" res)
    res))

Enable

Enable the tangle helper by running the following snippet with (org-ctrl-c-ctrl-c) mapped by default to C-c C-c.

⚠️ [2023-07-01 Sat] I just had issues running this using the C-c C-c binding and ended up navigating to the expression and just evaluating it (using C-c C-e which maps to eval-last-sexp).

(advice-add 'org-babel-tangle :around #'my-dotfiles-tangle-wrapper)

Before running this, you may want to auto-accept evaluations of code blocks. I’ve written vidbina/toggle-local-org-confirm-babel-evaluate in Org-Export Helpers in order to avoid having to confirm that I want to evaluate every block in my literate config through separate confirmations. Trust me, it’s not scalable to type “yes” over 30 times. 😅 Just run M-x vidbina/toggle-local-org-confirm-babel-evaluate and be happy!

Disable

Disable the tangle helper by running the following snippet with (org-ctrl-c-ctrl-c) mapped by default to C-c C-c.

(advice-remove 'org-babel-tangle #'my-dotfiles-tangle-wrapper)

<<install-hm>> Install with Home-Manager

My Emacs version is pretty up-to-date as per [2023-10-04 Wed 22:39] and was built entirely through my Nix configuration since I don’t want to be bothered in terms on tooling in figuring out how to rebuild Emacs from scratch whenever needed.

(emacs-version)
"GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, X toolkit, cairo version 1.16.0, Xaw3d scroll bars)"

The good bit about home-manager is that is will go as far as setting up home directories for me as well so there is little that I need to manually do other than the following steps:

  1. clone this repo to a path of choice (e.g.: ~/src/THIS_REPO)
  2. to install either
    1. my entire config:
      1. enter root of the repo and
      2. run make install
    2. or just my Emacs-related config:
      1. borrow ideas from default.nix (or default-darwin.nix for macOS users) and place them into your own home-manager configuration
  3. profit 💰

For myself: Run the following shell commands in order to set up symlinks for the personal.el and lang.el files (to my example files).

ln -s personal-example.el personal.el
ln -s lang.example.el lang.el

<<install-manual>> Install manually

<<emacsconfdir>>

⚠️ For this configuration we will refer to your emacs configuration directory as ~/.emacs.d for historic and didactic reasons, but it may be ~/.config/emacs in your case. Consult How Emacs Finds Your Init File for explanation on how Emacs loads configurations and note that every mention of ~/.emacs.d in this literate config assumes that you substitute it for your actual emacs configuration directory.

Clone this repository and symlink ~/.emacs.d into the emacs directory of this configuration:

  1. clone this repo to a path of choice (e.g.: ~/src/THIS_REPO)
  2. symlink ~/.emacs.d=[fn:3] to the =emacs subdirectory of the cloned project (e.g.: ln -s ~/src/THIS_REPO/emacs ~/.emacs.d)
  3. optionally, populate ~/.emacs.d/lang.el or ~/.emacs.d/personal.el (refer to Usage for instructions)
  4. reload your config or restart Emacs (e.g.: I have to run systemctl --restart emacs.service on NixOS since I am running Emacs as a systemd-managed service)

⚠️ Note that we are in subdirectory emacs of a dotfiles repository here which is all examples of commands or paths are written from the perspective of the top-level directory of this repository.

Usage

Use this configuration by changing this README.org file, the only source of truth, and tangling it to produce the Emacs LISP files early-init.el, init.el, lang.el and personal-example.el along with the relevant Nix configuration files default.nix and default-darwin.nix using the (org-babel-tangle) function which is mapped to C-c C-v C-t or C-c C-v t by default.

The files of this configuration are as follows:

README.org
source of truth that describes the entire configuration and tangles into the Elisp files listed below
early-init.el
configuration that is loaded before the GUI and package system are started (see The Early Init File)
init.el
primary configuration file that Emacs loads on start (see The Emacs Initialization File),
lang.el
optional configuration file for language-specific settings, and
personal-example.el
reference for personal.el where you can add your personally sensitive configuration.
personal.el
optional configuration file for personal setting which can be populated by copying personal-example.el for to get started and modifying the code as necessary
default.nix
Nix home-manager configuration for GNU/Linux systems
default-darwin.nix
Nix home-manager configuration for Darwin system (macOS)

images/conf-setup.png

For convenience’s sake, let’s start stubbing some of the files that we will be tangling from this literate configuration.

Tangle providence notice

;; Tangled from dotfiles/emacs/README.org

early-init.el

<<tangle-providence>>

init.el

<<tangle-providence>>

lang.el

⚠️ We only populate into an example file, named lang.example.el.

<<tangle-providence>>

personal.el

⚠️ We only populate into an example file, named personal-example.el.

<<tangle-providence>>

early-init.el

The early-init.el will be loaded before our “real configuration” is evaluated. Some configuration settings may have to be set at this stage but this should be used sparingly as it may be an indication of poor configuration when one has to resort too often to configuring at this stage.

(message "🥱 Loading early-init.el")

init.el

We enabe lexical binding, since some packages (e.g.: consult) will require this.

;; -*- lexical-binding: t -*-
(message "🚜 Loading init.el")

Package Management

Setting package-enable-at-startup to nil before the Emacs default package system even loads (i.e.: before early-init.el) minimizes the potential for global state to affect the configuration which ends up simplifying this configuration’s use[fn:4].

(setq package-enable-at-startup nil)

Straight

Straight.el 🍀 is a popular package manager used to manage Emacs packages.

The primary advantage of using straight.el is the ability to pin package version in a lockfile (e.g.: ~/.emacs.d/straight/versions/default.el) in manner quite similar to how popular package managers such as Bundler (Ruby) and NPM (JavaScript) or Yarn (JavaScript) improve reproducibility of a configuration by pinning the versions of their packages in a dependency manifest (e.g.: Gemfile.lock for Bundler, package.lock for NPM and yarn.lock for Yarn).

Pre-bootstrap Work-around

The issue is that straight relies on the existence of variables with prefixes that have been renamed from comp to native-comp. So, if the installed variant of Emacs lacks native compilation capability, then straight will be bumping into undefined symbols.

;; https://github.com/raxod502/straight.el/issues/757#issuecomment-839764260
(defvar comp-deferred-compilation-deny-list ())

Installation of the following packages may break when this block is disabled:

Bootstrap

Run the following bootstrap 🥾 logic at the start of your file:init.el in order to get your straight set up as our package manager.

;; https://github.com/radian-software/straight.el#getting-started
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

Enabling the commonly-used use-package interface

For convenience, we configure straight.el to use the use-package interface.

;; https://github.com/radian-software/straight.el#integration-with-use-package
(straight-use-package 'use-package)

In order to improve visibility over package-related issues, we set use-package-verbose such that loading and configuration information is verbosely reported. The reporting output can be examined in the *Messages* buffer or in the logging output (.e.g: systemd journal in case Emacs is run as a systemd user unit or service).

(setq use-package-verbose t)

<<use-package-format>> If you’ve used Emacs for a while or have read through a few configurations, you have likely encountered a use-package declaration before. An association list (alist) style interface (of keyword[fn:5]-value pairs) is used by use-package to provide a pleasantly readable configuration structure.

The Keywords page provides guidance as to how to use use-package keywords and the following snippet demonstrates the general structure of a use-package declaration:

;; Just an example of a use-package form
(use-package my-package-y
  :straight t
  :after
  (:all my-package-a my-package-x)

  :init
  (setq my-package-coefficient 42)
  (message "Just a heads-up: we'll be setting up package y")

  :config
  (my-package-y-run-checks-after-load)
  (my-package-y-check-data-on-filesystem)
  (my-package-y-mode t)

  :bind (("C-c y 1" . my-package-y-do-thing-in-buffer)
         ("C-c y 2" . my-package-y-do-another-thing-in-buffer)))

Organization

On the tidying up front, for example, you’ll find that the use-package macro provides mechanisms to:

  1. order the load sequence of packages by defining dependencies by means of the :after keyword
  2. time application of parts of a configurations before or after package load by means of the :init (i.e.: before load) and :config (i.e.: after load) keywords
  3. bind keychords using the :bind keyword

Performance

On the performance front, you’ll find that the use-package macro provides mechanisms to:

  1. delay loading of packages by means of the :defer or :demand keywords
  2. delay loading of packages needed in a particular mode or interpreter by means of the :mode or :interpreter keywords
  3. define “as-of-yet not seen” symbols that will be needed for compilation by means of the :functions and :defines keywords

Ordering Management through Hooks

Emacs is a hot mess of global state sorcery and as such it may be useful to load packages or call package-specific functions in a particular order to render a configuration sufficiently functional. 🙊

Using use-package, the :after and :hook keywords are probably the more powerful tools to manage the ordering of your packages.

The Emacs Startup Summary page outlines when the before-init-hook, after-init-hook, emacs-startup-hook and window-setup-hook are run.

For convenience, we define some hook to notify us in the Messages buffer when these different milestones are reached.

(add-hook 'before-init-hook (lambda () (message "🪝 Before init")))
(add-hook 'after-init-hook (lambda () (message "🪝 After init")))
(add-hook 'emacs-startup-hook (lambda () (message "🪝 Emacs startup")))
(add-hook 'window-setup-hook (lambda () (message "🪝 Window setup")))

Through the :hook keyword, we can hook operations for a particular package into to the previously listed Emacs lifecycle hooks without leaving the expression for that specific package – thus keeping all relevant configurations neatly localized.

Explain: Introspection

For troubleshootings sake it is helpful to know how to use function straight-dependents

;; test
(straight-dependents)
(straight-dependents 'org-roam)
(straight-dependencies)
(straight-dependencies 'org-roam)

Eldoc

(with-eval-after-load 'eldoc
  (setq eldoc-echo-area-prefer-doc-buffer t))

Org

Org is probably the killer app of Emacs and is actually just a clearly standardized markup format. Three ways in which Org discerns itself from Markdown are in that it:

  1. has a single clear standard (that is widely used) as opposed to Markdown that has a few variants floating about that exhibit slightly differing behavior[fn:6] and may present a bit of challenge for application developers that wish to implement the standard
  2. natively allows for the notation of dates and times which allow for things like time-tracking and planning within a single document.
  3. natively provides table support
;; https://orgmode.org/worg/org-contrib/org-protocol.html
;; https://github.com/org-roam/org-roam/issues/529
;; https://git.savannah.gnu.org/cgit/emacs/org-mode.git/
(use-package org
  :straight (:type built-in)
  :init
  (setq org-adapt-indentation nil ; https://orgmode.org/manual/Hard-indentation.html
        org-hide-leading-stars nil
        org-odd-levels-only nil)
  <<org-bind>>
  :config
  ;; https://orgmode.org/manual/Capture-templates.html#Capture-templates
  (global-set-key (kbd "C-c c") 'org-capture)
  (global-set-key (kbd "C-c d") 'org-hide-drawer-toggle)
  ;; https://www.reddit.com/r/emacs/comments/ldiryk/weird_tab_behavior_in_org_mode_source_blocks
  (setq org-src-preserve-indentation t
        org-hide-block-startup t)
  <<org-config>>
  :custom
  <<org-custom>>)

Customizations

(org-tags-column 0 "Avoid wrapping issues by minimizing tag indentation")
(org-catch-invisible-edits 'error "Disable invisible edits")
(org-src-window-setup 'current-window "Show edit buffer in calling window")
(org-refile-targets '((nil . (:maxlevel . 3))) "Allow refiling to 3rd level headings")

Set $LaTeX$ formula format

(org-format-latex-options '(
                            :foreground default
                            :background default
                            :scale 4.0
                            :html-foreground "Black"
                            :html-background "Transparent"
                            :html-scale 1.0
                            :matchers ("begin" "$1" "$" "$$" "\\(" "\\[")))

Zoom LaTeX formulas by adjusting the scale

Variable org-format-latex-options has the :scale attribute which informs how large LaTeX is rendered.

$$f(g(x))$$

(defcustom vidbina/latex-formula-zoom-step 1.2
  "The zoom increment to apply at very latex-formula-zoom step")

(defun vidbina/latex-formula-zoom-increase ()
  (interactive)
  (vidbina/latex-formula-zoom vidbina/latex-formula-zoom-step))

(defun vidbina/latex-formula-zoom-decrease ()
  (interactive)
  (if (eq 0 vidbina/latex-formula-zoom-step)
      (error "🔍 Can not zoom LaTeX with factor zero")
    (vidbina/latex-formula-zoom (/ vidbina/latex-formula-zoom-step))
    ))

(defun vidbina/latex-formula-zoom-reset (scale)
  (interactive (list (read-number "Reset to which scale? " 1 1)))
  (if (eq 0 vidbina/latex-formula-zoom-step)
      (error "🔍 Can not zoom LaTeX with factor zero")
    (setq org-format-latex-options
          (plist-put org-format-latex-options :scale scale))
    ))

(defun vidbina/latex-formula-zoom (factor)
  (setq org-format-latex-options
        (plist-put org-format-latex-options
                   :scale (* (plist-get org-format-latex-options :scale) factor))))

(advice-add 'default-text-scale-increase :after (lambda () (vidbina/latex-formula-zoom-increase)))

(advice-add 'default-text-scale-decrease :after (lambda () (vidbina/latex-formula-zoom-decrease)))

Default workflow states

To facilitate my workflow states, we define:

TODO
something we want to work on
WIP
something that is work-in-progress but not yet “usable”
PROTOTYPE
something that is “usable”
CANCELED
something that we aborted and we’ll have to provide explanation
DONE
something that works and is ready for prime-time
  • this state is struck-out because we should assume that any heading without a state is either done or not a task-like heading at all
  • in practice, I sometimes unstate headings with a DONE state after I feel enough time has passed to just turn them into boring headings
(org-todo-keywords '((sequence "TODO(t)" "WIP(w)" "|" "DONE(d)" "CANCELED(@c)")) "Allow fast-selection for my standard TODO states")

After a while, we can drop the DONE state from headings to just promote them to regular “features”. In fact, we may as well remove

images/org-default-workflow-states.png

Use GMT/UTC as timestamp TZ

When travelling, the default Emacs config allows for timestamps to just be framed in the current system timezone which dynamically changes on macOS. Just to keep thing from breaking, we want to lock our system such that times are always framed in the same TZ.

<<org-babel>> Org-Babel

<<org-babel-tangle>> Asynchronous Tangle

  • State “PROTOTYPE” from “WIP” [2022-06-28 Tue 13:34]
    initial concept using find-file which may be inefficient but we’ll eat the gun on account of the ease of use

Tangling sometimes takes a fair amount of time that we can’t always afford to waste by simply waiting. The following org-babel-tangle-async function wraps the original org-babel-tangle call in an async handler in order to allow us to regain control of our buffer while Org does its tangling magic.

(defun org-babel-tangle-async (&optional arg target-file lang-re)
  "Call `org-babel-tangle' asynchronously"
  (interactive "P")
  (message "🧬 Async Org-Babel: start tangle [%s]" buffer-file-name)
  <<org-babel-tangle-async-start>>)

The org-babel-map is defined in org-keys.el and maps t and C-t to org-babel-tangle of which we only need one alternative mapped to the async variant. Since I use lower keystroke variant C-v C-c t most, we’ll map that one to our async variant.

:bind (:map org-babel-map ("t" . org-babel-tangle-async))

The async handler, doesn’t have to copy over the buffer contents since saving is a prerequisite to tangling.

(run-hooks 'org-babel-pre-tangle-hook)

The async handler disables the auto-save facility and clears the pre-tangle hooks (perhaps a bad idea 🤷🏿‍♂️). We then just simply check that buffer-file-name is a string representing a valid file name and then open the file, navigate to point and trigger the intended =org-babel-tangle= call.

It’s unclear if the use of find-file is a really bad idea. I’ve considered that with-temp-buffer in combination with insert may be faster (see Xahlee), but since we’re tangling we’d need to spawn Org-mode anyways, so using find-file handles the major-mode switching for us to ensure that Org facilities are within reach. I may have to initialize packages if we need to mirror more of the configuration of the calling environment but we’ll cross that bridge when we get there – hence prototype. 🐉

(async-start `(lambda ()
                (message "🧬 Async Org-Babel: lambda start")
                (if (and (stringp ,buffer-file-name)
                         (file-exists-p ,buffer-file-name))
                    (progn
                      (setq exec-path ',exec-path
                            load-path ',load-path
                            enable-local-eval t
                            auto-save-default nil
                            org-babel-pre-tangle-hook '())
                      (print (format "🧬 Async Org-Babel: exec from [%s] load from [%s]" exec-path load-path))
                      (package-initialize)
                      (print (format "🧬 Async Org-Babel: package init completed"))

                      (find-file ,(buffer-file-name))
                      (print (format "🧬 Async Org-Babel: file [%s] found" ,buffer-file-name))
                      (read-only-mode t)
                      (goto-char ,(point))
                      (print (format "🧬 Async Org-Babel: point [%s] located" ,(point)))

                      (print (format "🧬 Async Org-Babel: auto confirm babel eval"))
                      (setq-local org-confirm-babel-evaluate nil)

                      (print (format "🧬 Async Org-Babel:\n\targ [%s]\n\ttarget [%s]\n\tlang [%s]" ,arg , target-file ,lang-re))
                      (org-babel-tangle ,arg ,target-file ,lang-re) ; tangle! (ref:org-babel-tangle-call)
                      (print (format "🧬 Async Org-Babel: tangled"))
                      buffer-file-name)
                  (error "🧬 Async Org-Babel: not visiting a file")))
             `(lambda (result)
                (message "🧬 Async Org-Babel: completed [%s]" result)))

Contrast current with org-export-async-start implementation

Is there something here that we can borrow to bring our implementation more in line with existing ways to handle async export.

<<ox-html>> ox-html: Org-Babel HTML exporter

When exporting HTML, it is quite frustrating when the exported HTML conflicts with the current theme in Emacs. When I’m using the dark theme and switch to the browser to view the exported HTML, browser extensions like darkmode typically don’t work on local files and thus switching between Emacs and the browser may result to a bit of a briefly-blinding constrast shock.

Background: Color retrieval from active theme

We define the following helper to retrieve currently active color foregrounds for a face of interest:

(let* ((sym (intern-soft (format ":%s" attribute)))
       (val (face-attribute face sym nil t))) ;; pass inherit 'nil to test none
  (if (or (null val) (eq 'unspecified val)) "none" (format "%s" val)))

Note that the default style is defined in org-html-style-default and is to be left alone (according to the Emacs docs). Observe the <a href=”https://orgmode.org/manual/CSS-support.html “>CSS support documentation for some explanation of some of the CSS classes that are used by ox-html.

(face-attribute 'default :foreground nil t)
(face-attribute 'default :background nil t)
(face-attribute 'link :foreground nil t)

Note how we can use the ox-html-get-color block defined above to retrieve colors in a code block.

(list :default <<ox-html-get-color(face='default)>>
      :link <<ox-html-get-color(face='link)>>
      :link-visited <<ox-html-get-color(face='link-visited)>>)

The resulting block with noweb is enabled, produces the following code:

(list :default "<<ox-html-get-color(face='default)>>"
      :link "<<ox-html-get-color(face='link)>>"
      :link-visited "<<ox-html-get-color(face='link-visited)>>")

CSS Template

The ox-html-get-color logic defined above can be used to template some CSS, agreeing with the current active theme in Emacs, that we can inject into exported HTML.

The variable org-html-style-default is not meant to be changed but the snippet below is derived therefrom and is drafted as a noweb template to aid in dynamically generating the needed CSS. We can tangle the snippet below into the ox-html.css file that we can use in our templates.

⚠️ Do not tangle this block async (so just use org-babel-tangle) because the theming information is not available when used through org-babel-tangle-async. I don’t know why this is but this thread may provide clues to a future debugger. 🕵🏿‍♂️

body {
  background-color: <<ox-html-get-color(face='default, attribute="background")>>;
  color: <<ox-html-get-color(face='default)>>;
}
a {
  color: <<ox-html-get-color(face='link)>>;
}
.todo {
  color: <<ox-html-get-color(face='org-todo)>>;
}
.done   {
  color: <<ox-html-get-color(face='org-done)>>;
}
.priority {
  color: <<ox-html-get-color(face='org-priority)>>;
}
.tag    {
  color: <<ox-html-get-color(face='org-tag)>>;
  background-color: <<ox-html-get-color(face='org-tag, attribute="background")>>;
}
.timestamp {
  color: <<ox-html-get-color(face='org-date, attribute="foreground")>>;
}
.timestamp-kwd {
  color: <<ox-html-get-color(face='org-scheduled)>>;
}
pre {
    border-color: <<ox-html-get-color(face='org-block)>>;
    background-color:  <<ox-html-get-color(face='org-block, attribute="background")>>;
    color: <<ox-html-get-color(face='org-block)>>;
}
pre.src:before {
    color: <<ox-html-get-color(face='org-block-begin-line)>>;
    background-color: <<ox-html-get-color(face='org-block-begin-line, attribute="background")>>;
}
.inlinetask {
    color: <<ox-html-get-color(face='org-level-8)>>;
    background-color: <<ox-html-get-color(face='org-level-8, attribute="background")>>;
}
.code-highlighted {
  background-color: <<ox-html-get-color(face='region, attribute="background")>>;
}
.org-info-js_search-highlight {
  background-color: <<ox-html-get-color(face='region, attribute="background")>>;
  color: <<ox-html-get-color(face='region)>>;
}

XeLaTeX instead of pdfLaTeX

The orgmode FAQ contains an entry on using XeLaTeX for LaTeX export instead of pdfLaTeX which we will use as a reference in order to simplify the export of my emoji-heavy files which are combined unicode characters which cause problems for pdfLaTeX.

Define reference to custom CS Template

In order to configure custom styling, we should define the string variable org-html-head (or its alias org-html-style) and clear org-html-head-include-default-style.

(org-html-head (format "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />" (expand-file-name "ox-html.css" user-emacs-directory)) "Point to our custom stylesheet")

CSS Generation Helpers

(defun vidbina/theme-switch-update-ox-html-css ()
  (let* ((colors '((:main-bg . (default :background))
                   (:main-fg . (default :foreground))
                   (:link-fg . (link :foreground))
                   (:todo-fg . (org-todo :foreground))
                   (:done-fg . (org-done :foreground))
                   (:prio-fg . (org-priority :foreground))
                   (:tag-fg . (org-tag :foreground))
                   (:tag-bg . (org-tag :background))
                   (:timestamp-fg . (org-date :foreground))
                   (:scheduled-fg . (org-scheduled :foreground))
                   (:org-block-fg . (org-block :foreground))
                   (:org-block-bg . (org-block :background))
                   (:org-block-edge-fg . (org-block-begin-line :foreground))
                   (:org-block-edge-bg . (org-block-begin-line :background))
                   (:org-block-edge-bg . (org-block-begin-line :background))
                   (:inlinetask-fg . (org-level-8 :foreground))
                   (:inlinetask-bg . (org-level-8 :background))
                   (:region-fg . (region :foreground))
                   (:region-bg . (region :background))
                   (:default . (default :foreground)))))
    (-> (mapcar
         (lambda (x)
           (let ((key (car x))
                 (val (pcase (cdr x)
                        (`(,a ,b) (let ((v (face-attribute a b nil t)))
                                    (if (eq 'unspecified v) nil (format "%s" v))))
                        (_ nil))))
             (list key (or val "none"))))
         colors)
        flatten-list))
  ;; TODO: Merge values into template
  )

Tips

Peeking through Org buffers

When navigating large buffers of Org, one can quickly peek through parts of the buffer in order to return to continue editing text thereafter. Do this by entering org-goto which is bound to C-c C-j by default and then using org-occur which is bound to / by default to have a glance and finally return by C-g.

Note that entering org-occur pulls up a buffer that provides some instructions.

Org-Tempo

Structure templates allow one to quickly produce convenient src or quote blocks with just three keypresses. For example

  • < s TAB to produce a source block
  • < q TAB to produce a quote block
  • < C TAB to produce a comment block
  • < h TAB and < l TAB to produce export blocks for HTML and LaTeX respectively
;; https://orgmode.org/manual/Structure-Templates.html
(require 'org-tempo)

Org $LaTeX$ Listing Source Wrapping

By default listings in LaTeX exports don’t line-wrap which is really useless when you have blocks with long lines and are hoping for readable output in a printable form-factor – not to say that you actually print it but hey… your e-reader may want that printable form-factor?!? 🤷🏿‍♂️

The following configuration was stolen from a Reddit thread.

Figuring out how to get the ouput portrayed in a monospace font was informed by a StackOverflow answer detailing how removal of the column=flexible option may help.

;; https://www.reddit.com/r/emacs/comments/c1b70i/best_way_to_include_source_code_blocks_in_a_latex/
(add-to-list 'org-latex-packages-alist '("" "listings" nil))
;;(setq org-latex-packages-alist nil)
;;(setq org-latex-listings t)
;;(setq org-latex-listings-options '(("breaklines" "true")))
(setq org-latex-listings t)
(setq org-latex-listings-options
      '(("basicstyle" "\\ttfamily")
        ("breakatwhitespace" "false")
        ("breakautoindent" "true")
        ("breaklines" "true")
        ;;("columns" "[c]fullflexible")
        ("commentstyle" "")
        ("emptylines" "*")
        ("extendedchars" "false")
        ;;("fancyvrb" "true")
        ("firstnumber" "auto")
        ("flexiblecolumns" "false")
        ("frame" "single")
        ("frameround" "tttt")
        ("identifierstyle" "")
        ("keepspaces" "true")
        ("keywordstyle" "")
        ("mathescape" "false")
        ("numbers" "left")
        ("numbers" "none")
        ("numbersep" "5pt")
        ("numberstyle" "\\tiny")
        ("resetmargins" "false")
        ("showlines" "true")
        ("showspaces" "false")
        ("showstringspaces" "false")
        ("showtabs" "true")
        ("stepnumber" "2")
        ("stringstyle" "")
        ("tab" "")
        ("tabsize" "4")
        ("texcl" "false")
        ("upquote" "false")))

ox-clip: export Org fragments into HTML for rich-text input fields

The ox-clip exporters allow us to export fragments of our org documents into rich-text that is ready to paste into inputs on web apps. I use this frequently to copy org pieces into Google Docs or other online rich-text editors.

;; https://github.com/jkitchin/ox-clip
;; https://zzamboni.org/post/my-emacs-configuration-with-commentary/
(use-package ox-clip
  :straight (ox-clip :type git
                     :host github
                     :repo "jkitchin/ox-clip")
  :after (org)
  :bind
  ("C-c y" . ox-clip-formatted-copy))

phscroll

The phscroll package allows to exclude some regions in a buffer from wrapping. This is really convenient when you have section with long lines that lose meaning when wrapped (or being unreadable) like rows from tables.

;; https://github.com/misohena/phscroll
(use-package phscroll
  :straight (phscroll :type git
                      :host github
                      :repo "misohena/phscroll")
  :init
  (setq org-startup-truncated nil)
  :config
  (with-eval-after-load "org"
    (require 'org-phscroll)))

Some of the bindings to remember when in phscroll-mode are:

C-x <, C-x >
horizontally scroll
C-l
recenter top-bottom
C-S-l
recenter left-right
  • twice to scroll to left edge
Name of something that could be very long, potentiallyFeature ColumnAnother Feature ColumnYes another feature column
Just to test that this works

Bibliography

ol-BibTeX (Org-BibTeX): Bibliography through org properties

The ol-bibtex package, previously known as org-bibtex and still prefixed as such, allows for the definition of bibliography entries within Org properties.

** Introduction to Flight Test Engineering
:PROPERTIES:
:BIB_TITLE: Introduction to Flight Test Engineering
:BIB_BTYPE: techreport
:BIB_CUSTOM_ID: stoliker2005FTE
:BIB_AUTHOR: F.N. Stoliker
:BIB_INSTITUTION: RTO
:BIB_YEAR: 2005
:BIB_NUMBER: RTO-AG-300-V14
:BIB_DATE: 7/25/2005
:BIB_ADDRESS:
:BIB_MONTH: 07
:BIB_BIB_DOI: 10.14339/RTO-AG-300-V14
:BIB_BIB_ISBN: 92-837-1126-2
:BIB_NOTE:
:BIB_ANNOTE:
:END:

Some notes on this book...

The previously listed Org snippet will produce the following BibTeX entry:

@techreport{stoliker2005FTE,
  annote={},
  note={},
  isbn={92-837-1126-2},
  doi={10.14339/RTO-AG-300-V14},
  month={07},
  address={},
  date={7/25/2005},
  number={RTO-AG-300-V14},
  year={2005},
  institution={RTO},
  author={F.N. Stoliker},
  custom_id={stoliker2005FTE},
  title={Introduction to Flight Test Engineering}
}

Please note that ol-bibtex refers to an internal index org-bibtex-types that lists fields for every record type (e.g.: article, book, techreport, etc.) and only honors the entries that are listed therein.

Since, I sometimes need “arbitrary” fields such as doi that BibTeX itself may recognize but that the ol-bibtex package will simply ignore (for some bibliography types) as they are not listed in org-bibtex-types, it will be necessary to set org-bibtex-export-arbitrary-types to honor arbitrary fields which itself will require org-bibtex-prefix to also be set (which I set to BIB_). The caveat is that setting org-bibtex-prefix is an all-or-nothing type of deal and will require us to prefix all BibTeX properties (with BIB_ in this particular configuration’s case).

Another option may be for us to enhance org-bibtex-headline to be a bit smarter about honoring “known fields” in a properties block along with “arbitrary fields” as long as they are prefixed. This is only a partial solution as it only solves to problem of converting headlines to BibTeX entries, while the ol-bibtex package also helps reading valid BibTeX entries with org-bibtex-read and writing them into Org headlines with org-bibtex-write where the prefix is used for all entries indicating that prefixing everything is the expected behavior that allows for reliable and consistent bidirectional traffic (Org-to-BibTeX and BibTeX-to-Org).

(use-package ol-bibtex
  :straight (:type built-in)
  :after org
  :custom
  (org-bibtex-prefix "BIB_" "Define prefix for arbitrary fields")
  (org-bibtex-export-arbitrary-fields t "Export prefixed fields"))

For reference’s sake, note that for headers containing non-prefixed and prefixed fields, ol-bibtex will end up exporting the prefixed fields only.

** Introduction to Flight Test Engineering
:PROPERTIES:
:TITLE:    Introduction to Flight Test Engineering
:BTYPE:    techreport
:CUSTOM_ID: stoliker2005FTE
:AUTHOR:   F.N. Stoliker
:INSTITUTION: RTO
:YEAR:     2005
:NUMBER:   RTO-AG-300-V14
:DATE:     7/25/2005
:ADDRESS:
:MONTH:    07
:BIB_DOI:  10.14339/RTO-AG-300-V14
:BIB_ISBN: 92-837-1126-2
:NOTE:
:ANNOTE:
:END:

The example listed above will yield the following BibTeX entry which demonstrates this point.

@techreport{stoliker2005FTE,
  isbn={92-837-1126-2},
  doi={10.14339/RTO-AG-300-V14}
}

Org-contrib

;; https://git.sr.ht/~bzg/org-contrib
(use-package org-contrib
  :straight (org-contrib :type git
                         :host nil
                         :repo "https://git.sr.ht/~bzg/org-contrib")
  :after org)

Org-Roam

A good solution for maintaining a Zettelkasten-inspired note-taking system is Org-Roam 🗄️ which allows one to conveniently link related notes together.

images/screenshot-orui-org-cite-dark.png

;; https://github.com/org-roam/org-roam
(use-package org-roam
  :straight (org-roam :type git
                      :host github
                      :repo "org-roam/org-roam")
  :after org
  :init
  (setq org-roam-v2-ack t)
  <<org-roam-init>>

  :config
  (message "📔 org-roam is loaded")
  <<org-roam-config>>

  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n i" . org-roam-node-insert)
         <<org-roam-bind>>))

Org-Roam Directory

Set the Org-Roam directory through variable org-roam-directory and keep in mind that this directory changes to the active directoy when switching between different Org-Roam directories.

💡 Multiple Org-Roam directories may be necessary to separate notes on a project or topic-specific basis.

A possible workaround to trigger the tangling from the root directory thata contains all note directories and letting Org-Roam recursively walk through the file tree to visit all those directories.

(let ((directory (file-truename "~/org/")))
  (make-directory directory t)
  (setq org-roam-directory directory
        ;; Define a directory that does not change along with the Org-Roam folder
        vidbina-org-roam-root-directory directory))

⚠️ This approach requires one to structure all Org-Roam directories within a single root directory

Use Markdown

Some of my notes, or some of my notes with others are unfortunately still in Markdown and I want to factor them into my Org-Roam setup.

(setq org-roam-file-extensions '("org" "md"))

Database

Set the Org-roam database location:

(setq org-roam-db-location (file-truename "~/org/roam/org-roam.db"))

<<org-roam-db-sync-async>> Async Org-Roam-DB Sync or rather Non-blocking Org-Roam DB Sync

Yes, naming of this section is problematic because of the async-sync bit but we’re trying to conduct the synchronization in an asynchronous manner such that we don’t have to block the main thread all the time we conduct an org-roam-db-sync so forgive me. We define interactive function vidbina/org-roam-async-forced-sync to expose the asynchronous synchronization logic.

(defun vidbina/org-roam-db-async-forced-sync ()
  "Force sync org-roam asynchronously"
  (interactive)
  <<vidbina/org-roam-db-async-sync>>)

Before triggering the async handler, we close all database connections in order to

  1. minimize issues with sqlite (which isn’t a multi-connection database) and to
  2. hopefully invalidate some cache such that subsequent Org-roam lookups or other events become change aware.
(org-roam-db--close-all)

that wraps the src_elisp[:exports code]{(org-roam-db-sync ‘force)} call in an asych handler.

The async handler is defined in the following block which allows us to quickly debug it by triggered org-edit-special on the block and executing the block by running eval-defun:

(let* ((label "🕷️ Async Org-Roam sync")
       (my-org-roam-vars '("org-roam-db-location"
                           "org-roam-file-extensions"
                           "org-roam-v2-ack"))
       (my-setq-form (async-inject-variables (regexp-opt my-org-roam-vars))))
  (async-start `(lambda ()
                  (message "%s start" ,label)
                  (setq exec-path ',exec-path
                        load-path ',load-path)
                  ,my-setq-form
                  (setq org-roam-directory ,vidbina-org-roam-root-directory)
                  (package-initialize)
                  (require 'org-roam)
                  (message "%s dir %s for file extensions %s"
                           ,label org-roam-directory org-roam-file-extensions)
                  (org-roam-db-sync 'force)
                  (with-current-buffer "*Messages*"
                    (buffer-string)))
               `(lambda (result)
                  (message "%s *Messages* was:\n%s\n%s end of report"
                           ,label result ,label))))

We define a binding that calls our async synchronization function:

("C-c n u" . vidbina/org-roam-db-async-forced-sync)

Since I want to handle synchronization of my Org-Roam database manually through my async handler, we disable autosync:

(org-roam-db-autosync-disable)

Org-Roam-UI

;; https://github.com/org-roam/org-roam-ui
(use-package org-roam-ui
  :straight (org-roam-ui :host github
                         :repo "org-roam/org-roam-ui"
                         :branch "main"
                         :files ("*.el" "out"))
  :delight
  (org-roam-ui-mode "🕸️")
  (org-roam-ui-follow-mode "👀")
  :after org-roam
  ;; normally we'd recommend hooking orui after org-roam, but since org-roam does not have
  ;; a hookable mode anymore, you're advised to pick something yourself
  ;; if you don't care about startup time, use
  :bind (("C-c n ." . org-roam-ui-node-zoom)
         ("C-c n ," . org-roam-ui-node-local))
  :hook (after-init . org-roam-ui-mode)
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow nil
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start nil))

Incorporate Markdown into your Org-Roam use

It isn’t unlikely that you will have some of your notes captured in Markdown files. In order to not have to rewrite these files into Org-files, you can use Md-roam.

;; https://github.com/nobiot/md-roam
(use-package md-roam
  :straight (md-roam :type git
                     :host github
                     :repo "nobiot/md-roam")
  :after org-roam
  :init
  (setq md-roam-use-markdown-file-links t
        md-roam-file_extension-single "md"
        org-roam-tag-sources '(prop md-frontmatter)
        org-roam-title-sources '((mdtitle title mdheadline headline) (mdalias alias))))

<<org-roam-bibtex>> Org-Roam-BibTeX (ORB)

;; https://github.com/org-roam/org-roam-bibtex
(use-package org-roam-bibtex
  :straight (org-roam-bibtex :type git
                             :host github
                             :repo "org-roam/org-roam-bibtex")
  :after org-roam
  <<org-roam-bibtex-org-ref>>
  )

We add a reminder in the source that usage of org-roam-bibtex in combination with org-ref requires additional configuration – lest I forget. 😅

;; NOTE: Using org-ref requires additional configuration

Org-QL

In order to query Org files with more flexibility, org-ql can come to the rescue.

;; https://github.com/alphapapa/org-ql
(use-package org-ql
  :straight (org-ql :type git
                    :host github
                    :repo "alphapapa/org-ql"))

In the most basic usage form you can basically run org-ql-search and just enter todo to get a basic listing.

<<ob-async>> ob-async

In some cases, code blocks need to be executed in a non-blocking manner (e.g.: when firing up a test instance of emacs or triggering a large file transfer). The ob-async package allow async execution of code-blocks by simply adding the :async keyword to the a codeblock of interest.

Since the <a href=”https://blog.tecosaur.com/tmio/2021-05-31-async.html#async-babel-sessions “>introduction of ob-comint.el there is support for sessions and async code execution built into Org itself. The only gotcha is that, at the time of writing [2022-06-26 Sun], there is only a Python implementation that is ob-comint compatible while there are implementations for R and Ruby in the works. For other runtimes, ob-async is a bit more flexible because we can use this on any language since there is no requirement on providing a ob-comint-compatible implementation but we simply spawn another Emacs sessions asynchronously to just run the code. Use variable ob-async-no-async-language-alist to bypass ob-async for org-babel-languages that provide their own :async keyword and async handling workflow.

Fork

In order to fix the ob-async for my own setup, I’ve forked the original repo with a few minor changes/fixes.

;; https://github.com/vidbina/ob-async
(use-package ob-async
  :straight (ob-async :type git
                      :host github
                      :branch "main"
                      :repo "vidbina/ob-async"))

Usage

Under the hood ob-async depends on async and can be used by specifying an :async header on a code block as demonstrated below:

sleep 2; echo hi

Introduce :async header args for org-lint checks

The :async header argument should be accepted by Org-lint when ob-async is loaded. IIRC the :async header arg should be legal for Python blocks.

Root cause

From a deeper analysis of async-inject-variables on the ob-async pattern, the following snippet will fail when the variable injection form is not “readable” by the async executor.

(read (pp-to-string (async-inject-variables ob-async-inject-variables)))

In some cases, variables like org-babel-hide-result-overlays may contain overlay values:

(overlayp (car org-babel-hide-result-overlays))

A sexp will be sent to the executor after serialization of the sexp through pp-to-string and since overlays are serialized into a #<overlays ...> format, they are not “readable” in their pretty-printed form:

(read (#<overlay from 39236 to 39236 in README.org<emacs>>))

Compose PR upstream

See PR astahlman/ob-async #88

Appearance

This section will deal with some of the visual trappings of Emacs. My design goal is to arrive at a rather minimal, or rather clean, design while providing the needed information scope perhaps through toggles (i.e.: showing whitespace characters at command).

Hide GUI Elements

In order to minimize visual noise, let’s disable the graphical scroll bars, tool bars and menu bars.

;; https://www.emacswiki.org/emacs/ScrollBar
(scroll-bar-mode -1)

;; https://www.emacswiki.org/emacs/ToolBar
(tool-bar-mode -1)

;; https://www.emacswiki.org/emacs/MenuBar
(menu-bar-mode -1)

;; https://www.emacswiki.org/emacs/ShowParenMode
(show-paren-mode 1)

Comint mode

During development, using the projectile-compile-project and projectile-test-project produces output in a comint buffer which we always want to be color coded. Emacs now has a builtin way to handle ANSI colors correctly according to this SO thread.

(use-package ansi-color
    :hook (compilation-filter . ansi-color-compilation-filter)) 

Furthermore, we want the content of the comint buffers to always be scrolled to the bottom such that I don’t need to manually scroll to the relevant section.

(customize-set-variable 'compilation-scroll-output t "auto-scroll to bottom")

Line Numbers

Let’s hide line numbers and For the sake of ease of navigation and spatial orientation we display line numbers in the left margin.

;; https://www.emacswiki.org/emacs/LineNumbers
(use-package display-line-numbers
  :straight (:type built-in)

  :config
  (display-line-numbers-mode 0)

  :hook
  (prog-mode . (lambda () (display-line-numbers-mode 1)))
  (notmuch-hello-mode . (lambda () (display-line-numbers-mode 0)))

  :bind
  (("C-c n n" . display-line-numbers-mode)))

Whitespace

Visualize white spaces (tabs, spaces, trailing whitespace). The global whitespace mode can be toggled through (global-whitespace-mode) in order to reduce the visual noise or enable the whitespace indication.

;; https://www.emacswiki.org/emacs/WhiteSpace
;; https://www.emacswiki.org/emacs?action=browse;oldid=WhitespaceMode;id=WhiteSpace
(setq whitespace-style '(empty face lines-tail tabs trailing))

Modeline

The modeline is the bar typically at the bottom of a buffer which provides useful information about the system.

Delight and Diminish

Since the amount of textual information in the Modeline can get overwhelming at times, we provide horizontally succinct (i.e.: single char) pictographic indicators for the Modeline instead.

;; https://git.savannah.nongnu.org/git/delight.git
(use-package delight
  :straight (delight :type git
                     :host nil
                     :repo "https://git.savannah.nongnu.org/git/delight.git")
  :delight
  (fundamental-mode "🗒️")
  (auto-revert-mode "♻️")
  (eldoc-mode "el📖")
  (edebug-mode "🐞")
  (whitespace-mode "🏳️")
  (visual-line-mode "🌯")
  (mu4e-main-mode "📫")
  (mu4e-headers-mode "📬")
  (mu4e-view-mode "📧")
  (vterm-mode "👨🏿‍💻"))

;; https://github.com/myrjola/diminish.el
(use-package diminish
  :straight (diminish :type git
                      :host github
                      :repo "myrjola/diminish.el"))

One can debug the configuration by examining the minor-mode-alist variable to verify if the delight/diminish configurations are correctly applied to the configuration variable.

Delight auto-revert mode

Set auto-revert-mode-text since the delight setting isn’t robust enough. Perhaps we should move this out into a dedicated use-package form for auto-revert-mode.

(customize-set-variable 'auto-revert-mode-text "♻️")

Text

<<visual-fill-column>> Visual Fill Column

For the sake of readability, it helps to wrap text at a fixed column instead of filling up whatever screen real estate that is available to a buffer. The visual-fill-column package by Joost Kremers accomplishes just this and can be toggled by running (visual-fill-column-mode).

;; https://github.com/joostkremers/visual-fill-column
(use-package visual-fill-column
  :straight (visual-fill-column :type git
                                :host github
                                :repo "joostkremers/visual-fill-column"))

images/demonstration-visual-fill-column.gif

Global binding for convenience

For convenience, I have defined the following global binding to facilitate my laziness and avoid having to enter visual-fill-column-mode which isn’t as much of a pain to begin with TBH if you consider that there is completion within Emacs. 🤷🏿‍♂️

(global-set-key (kbd "C-c v \\") 'visual-fill-column-mode)

<<adaptive-wrap>> Adaptive Wrap

By using adaptive wrap mode, wrapping behaviour can be adapted to respect indentation present at the start of a line. This should simply the readability of long lines in e-mail quotes or in nested code.

;; https://elpa.gnu.org/packages/adaptive-wrap.html
(use-package adaptive-wrap
  :straight (adaptive-wrap :type git
                           :host github
                           :repo "emacs-straight/adaptive-wrap")
  :config
  (adaptive-wrap-prefix-mode))

Using adaptive wrapping along with visual-fill-column mode may introduce some performance issues especially when longer texts are being soft-wrapped. When dealing with code blocks or tables, adaptie wrapping can be a bit more confusing than helpful which is why it helps to define key bindings to simplify toggling this behaviour. In my case, I have defined the vidbina/wrap function to play to control visual-line-mode and adaptive-wrap-mode in a single operation.

Default Text Scale

For global text scaling, the default-text-scale package can be used. Without this package, scaling may require one to resize the text in every buffer independently which is an arduous task.

;; https://github.com/purcell/default-text-scale
;; Doesn't work well in emacsclient
(use-package default-text-scale
  :straight (default-text-scale :type git
                                :host github
                                :repo "purcell/default-text-scale")
  :hook ((after-init . default-text-scale-mode)))

The package sets the height attribute of the default face, which can be retrieved by the following code:

(face-attribute 'default :height)

In my configuration, I have updated the height of the default face through the customize-face interface (see Customizing Specific Items for more instruction on how to customize faces in Emacs) and applied and saved these changes to allow different machines/environments to load their customization that don’t make much sense tracking in git (see the section on our custom.el file which allows for this).

⚠️ When updating the font face through the customize-face interface see to it that you uncheck all non-height attributes to ensure that the customization written into custom.el only sets height information as in the snippet example below:

(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(default ((t (:height 241)))))

Investigate if the server-after-make-frame-hook is a sane configuration option

The call to default-text-scale-reset has been configured in the server-after-make-frame-hook since I was having some trouble using this package when using Emacs in client/server mode as opposed to standalone mode.

I don’t quite use Emacs in standalone mode anymore, unless I’m debugging my config (by invoking emacs --debug-init), but AFAIK, the server-after-make-frame-hook was necessary to ensure that default-text-scale-reset is only called once the a GUI frame is ready, thus allowing default-text-scale to calculculate text scale.

Note that default-text-scale--update-for-new-frame is called in after-make-frame-functions, so perhaps this is already sufficient to ensure that all frames have the same scaling for text.

Olivetti

For focus, Olivetti mode can be a great help, so let’s just install it for the odd cases where we need it.

;; https://github.com/rnkn/olivetti.git
(use-package olivetti
  :straight (olivetti :type git
                      :host github
                      :repo "rnkn/olivetti"))

Rainbow Mode

Rainbow mode allows the coloring or color codes within buffers such as #ff0000 and #0f0.

;; https://github.com/emacsmirror/rainbow-mode
(use-package rainbow-mode
  :straight (rainbow-mode :type git
                          :host github
                          :repo "emacsmirror/rainbow-mode"))

Themes

Modus Themes

In order to avoid overthinking themes, I’ve opted for Prot’s Modus themes which offers a highly readable color scheme from an accessibility perspective.

images/screenshot-README-dark.png

images/screenshot-README-light.png

;; https://gitlab.com/protesilaos/modus-themes
(use-package modus-themes
  :straight (modus-themes :type git
                          :host gitlab
                          :repo "protesilaos/modus-themes")
  :custom
  <<modus-custom>>
  )

Misc Customizations

Use bold text in more code constructs
(modus-themes-bold-constructs t)
Set Org blocks background to gray to make them easier to spot
(modus-themes-org-blocks 'gray-background)
Use subtle or intense style for minibuffer and REPL prompts
(modus-themes-prompts '(intense))

Heading Customization

Heading typography customizations
(modus-themes-headings '((0 . (light 1.5))
                         (1 . (regular 1.3))
                         (2 . (regular 1.1))
                         (t . (regular 1.1))))
Heading color customizations
(modus-themes-common-palette-overrides
 '((bg-heading-1 bg-yellow-nuanced)
   (bg-heading-2 bg-blue-nuanced)
   (bg-heading-3 bg-green-nuanced)
   (bg-heading-4 bg-cyan-nuanced)
   (bg-heading-5 bg-red-nuanced)
   (fg-heading-0 fg-main)
   (fg-heading-1 fg-main)
   (fg-heading-2 fg-main)
   (fg-heading-3 fg-main)
   (fg-heading-4 fg-main)
   (fg-heading-5 fg-main)
   (fg-heading-6 fg-main)
   (fg-heading-7 fg-main)
   (fg-heading-8 fg-main)

   (prose-done green-intense)
   (prose-todo red-intense)))

Theme Magic

The theme-magic package uses pywal to update terminal emulator color schemes or themes. Using theme-magic allows one to syncs the terminal themes to the current Emacs theme.

(use-package theme-magic
  :straight (theme-magic :type git
                         :host github
                         :repo "jcaw/theme-magic")
  :config
  (theme-magic-export-theme-mode)
  <<theme-magic-config>>
)

Run the theme-magic-from-emacs command to update your system theme to match your Emacs theme.

Pywal provides instructions on the terminal emulators that are compatible and how to test them, note that the following terminals are known to not work well with pywal:

  • Konsole (the KDE standard)
  • Hyper (the web-based Vercel)
  • Terminal.app (the macOS standard)
  • Terminology (the Enlightenment EFL standard)
  • st (the suckless standard)

Improve theme output for Modus Themes

See theme-magic--preferred-extracted-colors for an overview of the output.

(setq theme-magic--preferred-extracted-colors
      '(
        ;; background
        (0 . ((modus-themes-get-color-value 'bg-main)
              (modus-themes-get-color-value 'bg-dim)))

        ;; error (red)
        (1 . ((modus-themes-get-color-value 'red-intense)
              (modus-themes-get-color-value 'err)
              (modus-themes-get-color-value 'red)))

        ;; warning (yellow)
        (3 . ((modus-themes-get-color-value 'yellow-warmer)
              (modus-themes-get-color-value 'warning)
              (modus-themes-get-color-value 'yellow-intense)
              (modus-themes-get-color-value 'bg-yellow-intense)))

        ;; cyan
        (6 . ((modus-themes-get-color-value 'cyan-intense)
              (modus-themes-get-color-value 'cyan)))

        ;; foreground
        (7 . ((modus-themes-get-color-value 'fg-main)))

        ;; alt/faded
        (8 . ((modus-themes-get-color-value 'fg-dim)
              (modus-themes-get-color-value 'fg-alt)))

        ;; additionals, non primaries

        ;; (green)
        (2 . ((modus-themes-get-color-value 'green-intense)
              (modus-themes-get-color-value 'green)
              (modus-themes-get-color-value 'green-faint)))

        ;; (blue)
        (4 . ((modus-themes-get-color-value 'blue-warmer)
              (modus-themes-get-color-value 'blue-intense)
              (modus-themes-get-color-value 'blue)))

        ;; (purple)
        (5 . ((modus-themes-get-color-value 'magenta-intense)
              (modus-themes-get-color-value 'magenta-warmer)
              (modus-themes-get-color-value 'magenta)))))

Icons: All the icons

;; https://github.com/domtronn/all-the-icons.el
(use-package all-the-icons
  :straight (all-the-icons :type git
                     :host github
                     :repo "domtronn/all-the-icons.el")
:if (display-graphic-p))

Post-installation, don’t forget to run all-the-icons-install-fonts.

Misc

YASnippets

;; https://github.com/joaotavora/yasnippet
(use-package yasnippet
  :straight (yasnippet :type git
                       :host github
                       :repo "joaotavora/yasnippet")
  :config
  (yas-global-mode 1))

Windows

Buffer Placement

(setq display-buffer-alist
      (let* ((sidebar-width '(window-width . 85))
             (sidebar-parameters '(window-parameters . ((no-other-window . t))))
             (sidebar (list '(side . left) sidebar-width sidebar-parameters)))
        (list (cons (regexp-opt-group '("*org-roam*"))
                    (cons #'display-buffer-in-side-window
                          `((slot . 0) ,@sidebar)))
              (cons (regexp-opt-group '("*ChatGPT*"))
                    (cons #'display-buffer-same-window
                          `((slot . 0) ,@sidebar)))
              (cons (regexp-opt-group '("*Dictionary*"))
                    (cons #'display-buffer-in-side-window
                          `((slot . -1) ,@sidebar)))
              (cons (regexp-opt-group '("*Help*" "*Info*" "*info*"))
                    (cons #'display-buffer-in-side-window
                          `((slot . 5) ,@sidebar)))
              (cons (regexp-opt-group '("*Shortdoc"))
                    (cons #'display-buffer-in-side-window
                          `((slot . 6) ,@sidebar)))
              (cons (regexp-opt-group '("*Warnings*"))
                    (cons #'display-buffer-in-side-window
                          `((slot . 10) ,@sidebar)))
              (cons (regexp-opt-group '("*dotfile-helpers*"))
                    (cons #'display-buffer-no-window
                          `())))))

Error reporting

Just did some updates and am getting a lot of warnings for docstrings being wider than 80 chars and more of that jazz. This is a massive distraction (for me) and does not warrant a buffer popping up to make me aware of the issues so we’re opting to just allow the buffer to emerge for actual errors instead and thus reducing the noisiness.

(customize-set-variable 'display-warning-minimum-level :error
                        "Pop up buffer for error-level or more severe warnings")

<<zoom-window>> Zoom-window: Zoom to a single window

In order to single out a particular window in order to return to the preceding layout shortly thereafter again, one may use the zoom-window package. It’s a great way to clear some screen real estate and obtain some focus.

;; https://github.com/emacsorphanage/zoom-window
(use-package zoom-window
  :straight (zoom-window :type git
                         :host github
                         :repo "emacsorphanage/zoom-window")
  :init
  (message "Configuring ‘zoom-window’")
  <<zoom-window-init>>)

<<ace-window>> ace-window

In order to quickly jump between windows by numbers, we can use the ace-window package. This eliminates the need for the tedious next/previous window bindings (either native Emacs or evil).

;; https://github.com/abo-abo/ace-window
;; https://jao.io/blog/2020-05-12-ace-window.html
(use-package ace-window
  :straight (ace-window :type git
                        :host github
                        :repo "abo-abo/ace-window")
  :bind (("M-o" . ace-window)))

Debug hanging when attempting to switchin windows through M-o from help page

When the help page for function null is open, the M-o binding just ends up hanging up Emacs sometimes. Can’t reproduce it yet. 😭

(describe-function #'null)

Profiler results
Profiler output from the null help page
Memory
1,362,566,599  99% - command-execute
1,359,050,303  99%  - funcall-interactively
1,355,593,128  99%   - ace-window
1,355,593,128  99%    - ace-select-window
1,355,593,128  99%     - aw-select
1,355,592,072  99%      - avy-read
       22,136   0%       - aw--lead-overlay
        6,336   0%        + #<compiled 0x21abf780a0956>
        6,136   0%          aw--point-visible-p
        5,280   0%        - aw--overlay-str
        2,112   0%         - select-window
        2,112   0%          - apply
        2,112   0%             ad-Advice-select-window
        2,112   0%         + #<compiled 0x21a7852646156>
        4,224   0%        - select-window
        4,224   0%         - apply
        4,224   0%            ad-Advice-select-window
    3,457,175   0%   + execute-extended-command
    3,515,240   0%  + byte-code
      249,481   0% + timer-event-handler
       47,172   0% + redisplay_internal (C function)
           24   0% + eldoc-schedule-timer
           21   0% + #<compiled 0xdb7dccf4d947c67>
            0   0%   ...
CPU
6135  59% - ...
6135  59%    Automatic GC
3875  37% + command-execute
 285   2% + timer-event-handler
   1   0%   redisplay_internal (C function)
Profiler output

This run was particularly problematic, as you can tell it has basically consumed a crapload of memory and GC seems to be going wild.

Memory
7,441,616,899  99% - command-execute
7,435,010,443  99%  - funcall-interactively
7,424,303,916  99%   - ace-window
7,424,303,916  99%    - ace-select-window
7,424,303,916  99%     - aw-select
7,424,302,860  99%      - avy-read
  133,056,380   1%       - aw--lead-overlay
      129,404   0%          aw--point-visible-p
        5,280   0%        + aw--overlay-str
        5,280   0%        + #<compiled 0x47199a3e5d956>
        4,224   0%        + select-window
        1,056   0%        aw--make-backgrounds
    8,762,616   0%   + describe-function
    1,936,327   0%   + execute-extended-command
    5,284,212   0%  + byte-code
    1,321,188   0%  + help-fns--describe-function-or-command-prompt
      130,504   0% + redisplay_internal (C function)
       92,686   0% + timer-event-handler
        5,224   0% + eldoc-schedule-timer
        2,112   0% + jit-lock--antiblink-post-command
        1,098   0% + #<compiled 0xdb7dccf4d947c67>
            0   0%   ...
CPU
36661  66% - ...
36661  66%    Automatic GC
18178  33% + command-execute
    8   0% + timer-event-handler
    3   0% + redisplay_internal (C function)
Profiler [2022-07-06 Wed 13:52]

Point on README.org and trying to M-o to Help bufer with org-html-head-include-default-style.

Memory
3,719,970,307  99% - command-execute
3,715,823,503  99%  - funcall-interactively
3,712,721,656  99%   - ace-window
3,712,721,656  99%    - ace-select-window
3,712,721,656  99%     - aw-select
3,712,717,432  99%      - avy-read
      390,744   0%       + aw--lead-overlay
        2,112   0%      + aw-window-list
        1,056   0%        aw--make-backgrounds
        1,056   0%      + #<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_26>
    3,101,847   0%   + execute-extended-command
    4,146,804   0%  + byte-code
       91,678   0% + timer-event-handler
       33,240   0% + redisplay_internal (C function)
        1,056   0% + mode-local-post-major-mode-change
           42   0% + #<compiled 0xd98e199f0b07c67>
           24   0% + eldoc-schedule-timer
            0   0%   ...
CPU
18122  53% - ...
18122  53%    Automatic GC
15631  46% + command-execute
   10   0% + timer-event-handler
    1   0% + #<compiled 0xd98e199f0b07c67>
Profiler [2022-07-06 Wed 13:55]

Point on README.org (avy 2) and ran M-o and the avy window labels appeared but pressing any number key yields no result. Tried entering 1 which should have moved point to the Help buffer and also tried 2 which should have kept point on README.org.

Memory
10,551,251  98% - command-execute
 5,733,544  53%  - byte-code
 5,729,400  53%   - read-extended-command
 5,729,400  53%    - completing-read-default
 5,729,400  53%     - apply
 5,729,400  53%      - vertico--advice
 4,813,351  45%       + #<subr completing-read-default>
 4,817,707  45%  - funcall-interactively
 3,694,391  34%   - execute-extended-command
 3,693,287  34%    - command-execute
 3,693,271  34%     - funcall-interactively
       631   0%      - profiler-start
       631   0%         apply
     1,104   0%    + run-at-time
 1,118,124  10%   - ace-window
 1,118,124  10%    - ace-select-window
 1,118,124  10%     - aw-select
 1,113,908  10%      - avy-read
   173,364   1%       - read-key
   147,140   1%        - read-key-sequence-vector
    10,812   0%         - redisplay_internal (C function)
    10,812   0%          - eval
    10,560   0%           - format
     5,280   0%            - propertize
     5,280   0%             - let
     5,280   0%                get-current-persp
     5,280   0%            - safe-persp-name
     5,280   0%               get-current-persp
     1,056   0%         + timer-event-handler
    17,424   0%        + use-global-map
     7,192   0%        + #<compiled 0x1449b5b163f5817e>
    75,200   0%       + aw--lead-overlay
     3,160   0%      + aw-window-list
     1,056   0%      + avy-tree
    74,096   0% + timer-event-handler
    37,416   0% + redisplay_internal (C function)
     1,080   0% + eldoc-schedule-timer
     1,056   0% + mode-local-post-major-mode-change
        63   0% + #<compiled 0xd98e199f0b07c67>
         0   0%   ...
CPU
8272  99% - command-execute
7685  92%  - funcall-interactively
7677  92%   - ace-window
7677  92%    - ace-select-window
7677  92%     - aw-select
7648  92%      - #<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_26>
7648  92%       - aw--done
5433  65%        - aw--restore-windows-hscroll
 775   9%           #<compiled 0xf5c1743c948468>
  28   0%      + avy-read
   1   0%      + aw-window-list
   8   0%   + execute-extended-command
 587   7%  + byte-code
   4   0% + timer-event-handler
   3   0% + redisplay_internal (C function)
   1   0% + #<compiled 0xd98e199f0b07c67>
   0   0% + ...
Yak Shaving

Let’s study what avy-tree really does. The signature is (avy-read TREE DISPLAY-FN CLEANUP-FN) and is defined in avy.el.

<<avy>> avy

In order to speed up text navigation, one can use avy to produce jump points that one can navigate through single keystrokes.

In order to jump to bind in the snippet below, one can grep for bind which is often fast enough or… one can trigger (avy-goto-char), type b and then observe how the different occurrences of b provide an indication of the character (or sequence of characters) that we need to press to “teleport” to that location.

;; https://github.com/abo-abo/avy
(use-package avy
  :straight (avy :type git
                 :host github
                 :repo "abo-abo/avy")
  :bind (("C-:" . avy-goto-char)))

Rotate

Akin to rotating layouts in tmux, emacs-rotate helps users rotate through layouts in Emacs. This can be handy when you quickly want to change a vertically tiles layout into a horizontally tiled layout.

;; https://github.com/daichirata/emacs-rotate
(use-package rotate
  :straight (rotate :type git
                    :host github
                    :repo "daichirata/emacs-rotate"))

Find File at Point (FFAP)

In order to provide point-specific behavior, we use the FFAP package. As an example, the (find-file-at-point) command will provide custom behavior depending on the type of link it is called over.

;; https://www.gnu.org/software/emacs/manual/html_node/emacs/FFAP.html#index-ffap
(ffap-bindings)
(defun vidbina/get-likely-current-directory ()
  (interactive)

  (let ((file-buffers (seq-filter (lambda (x) (buffer-file-name x)) (persp-get-buffers))))
    (cond
     ;; If buffer has filename, use its directory
     ((not (eq (buffer-file-name) nil))
      (progn
        (message "📁 Buffer has file name so, returning dir of buffer file")
        (file-name-directory buffer-file-name)))
     ;; If file buffers exist, pick the directory of the first one
     ((> (length file-buffers) 0)
      (file-name-directory (buffer-file-name (car file-buffers))))
     ;; Otherwise, homedir
     (t "~/"))))

(defun vidbina/ffap-vterm-in-persp-mode ()
  "Handle FFAP within persp-mode"
  (interactive)
  (message "🛣️ FFAP: figuring out dir for vterm")
  (let ((likely-directory (vidbina/get-likely-current-directory)))
    (if (eq likely-directory nil)
        (warn "🛣️ FFAP: No likely directory, so nothing")
      (message "🛣️ FFAP: likely dir = %s" likely-directory)
      (find-file-at-point likely-directory))))

Indentation

Turn of tab-indentation and opt for space-based indentation such that whitespace is a bit more controllable.

;; https://www.gnu.org/software/emacs/manual/html_node/eintr/Indent-Tabs-Mode.html
(setq-default indent-tabs-mode nil)

⚠️ Not to append to ongoing flame wars: across different editors and viewers (pagers, terminals, etc) the use of spaces is a bit more predictable as a text alignment tool. 🤷🏿‍♂️

Scrolling

In order to facilitate smoother scrolling than the default i.e.: “when scrolling out of view, scroll such that point is in the middle of the buffer”, we set scroll-conservatively to allow for more line-by-line scrolling.

;; https://www.emacswiki.org/emacs/SmoothScrolling
(setq-default scroll-conservatively 100)

💡 If you want to center the cursor (or point in Emacs vernacular), the evil-scroll-line-to-center command bound to z z is your friend.

Undo

;; https://github.com/emacsmirror/undo-fu
(use-package undo-fu
  :straight (undo-fu :type git
                     :host github
                     :repo "emacsmirror/undo-fu"))

<<async>> Async

Emacs is single-threaded and this makes sense considering that many packages navigate the live buffers or affect change to these buffers. Just imagine the mess if these packages attempted to conduct these operations on Emacs buffers concurrently. 😧

Emacs async allows for some async code execution which can come in handy for logic that may otherwise have blocked the Emacs main thread for too long.

;; https://github.com/jwiegley/emacs-async
(use-package async
  :straight (async :type git
                   :host github
                   :repo "jwiegley/emacs-async")
  :config
  <<async-config>>
  :custom
  <<async-custom>>)
(async-bytecomp-package-mode 1)
(async-variables-noprops-function #'async--purecopy)

iedit

The evil-search-forward (bound to /) which triggers isearch-forward under the hood allows for the temporary highlighting of entered patterns which can provide awareness of a patterns presence in a buffer but sometimes one just wants to highlight a symbol under point without having to type it in first.

;; https://github.com/victorhge/iedit
(use-package iedit
  :straight (iedit :type git
                   :host github
                   :repo "victorhge/iedit"))

Enable the iedit-mode through the C-; binding to highlight symbol under point throughout the buffer.

<<evil>> Evil

In order to save my hands some pain, it is helpful to use vi-like bindings that keep your hands around the home row more often and minimizes the need for your hands to pull acrobatic maneuvers 🎪 that could incur some strain – those Emacs key-chords. I use the extensible vi layer, inconveniently but mischievously abbreviated to Evil, to help me to vi-bindings while in Emacs.

I used classical Emacs with the typical bindings extensively in college[fn:college:around the end of the early 2000s as I started college in 2007] and developed a pretty rough case of the Emacs pinky issue at the time. That’s about the time I switched back to *vi? (vi, gvim, vim) and around the end of 2021, I decided to give Emacs another try in combination with Evil-mode which provides me the best of both worlds. 🤯

Consult the guide for more information on evil. Note that the vi commands started with colon such a :e, :s and :g are mapped through evil-ex (see agzam’s write up on these evil-ex commands for reference).

;; https://github.com/emacs-evil/evil
;; https://github.com/noctuid/evil-guide
(use-package evil
  :straight (evil :type git
                  :host github
                  :repo "emacs-evil/evil")
  :after
  undo-fu
  :init
  ;; pre-set some evil vars prior to package load
  (setq evil-respect-visual-line-mode t)
  (setq evil-undo-system 'undo-fu)
  (setq evil-want-integration t)
  (setq evil-want-keybinding nil)
  (setq evil-mode-line-format nil)
  :config
  (message "😈 Configured evil-mode"))
;; https://github.com/emacs-evil/evil-collection
(use-package evil-collection
  :straight (evil-collection :type git
                             :host github
                             :repo "emacs-evil/evil-collection")
  :after evil
  :custom
  (evil-collection-setup-minibuffer t)
  :config
  (evil-mode 1)
  (message "😈 Enable evil-mode")
  (evil-collection-init)
  (advice-add 'evil-collection-mu4e-setup
              :before (lambda ()
                        (message "😈 Setup up evil-collection for mu4e 📧")))
  (advice-add 'evil-collection-vterm-setup
              :before (lambda ()
                        (message "😈 Setup up evil-collection for vterm 📠")))
  :delight
  (evil-collection-unimpaired-mode "🚀"))

🚨 Remember the =C-z= binding to exit the ‘emacs state and return to ‘normal state. You may accidentally change evil-state (evil states are the equivalent to modes in vim vernacular) to emacs which will leave you with really annoying results when attempting to quickly navigate/edit your buffers. I’ve been cursing often enough thinking that my config was broken 🤬 when I had just accidentally pressed C-z and ended up in Emacs state. 🤦🏿‍♂️

People have somewhat strong opinions about keybindings. Grab yourself some popcorn 🍿 and enjoy some choice words on Reddit or on a number of YouTube videos that are pretty easy to find.

Read Living in Evil (Aaron Bieber) for some insight into some evil struggles that you may run into.

<<evil-vimish-fold>> Folding in a “vimish” fashion

The evil-vimish-fold package does exactly what the name implies and integrates evil and vimish-fold such that we can fold regions through the vim-like bindings with classics such as z f to fold, z o to open and z d to delete.

;; https://github.com/alexmurray/evil-vimish-fold
(use-package evil-vimish-fold
  :straight (evil-vimish-fold :type git
                              :host github
                              :repo "alexmurray/evil-vimish-fold")
  :diminish evil-vimish-fold-mode
  :after
  (:all vimish-fold)
  :hook ((prog-mode conf-mode text-mode) . evil-vimish-fold-mode))

<<vimish-fold>> Folding with vimish-fold

The vimish-fold package allows us to fold regions in buffers while persisting our folding preferences when we save files.

;; https://github.com/matsievskiysv/vimish-fold
(use-package vimish-fold
  :straight (vimish-fold :type git
                         :host github
                         :repo "matsievskiysv/vimish-fold")
  :after evil)

Annotation

The annotate.el package allows us to annotate code in different projects without affecting those project directories themselves. Think of it as your personal marker/highligher toolbox for source code.

;; https://github.com/bastibe/annotate.el
(use-package annotate
  :straight (annotate :type git
                      :host github
                      :repo "bastibe/annotate.el")
  :custom
  (annotate-file-buffer-local nil "Use central annotations file"))

Use C-c C-a to add annotations and C-c C-d to delete annotations.

Version Control

Magit: Git Porcelain

;; https://github.com/magit/magit.git
(use-package magit
  :straight (magit :type git
                   :host github
                   :repo "magit/magit"
                   :branch "main")
  :custom
  (magit-display-buffer-function
   (lambda (buffer)
     ;; based on magit-display-buffer-same-window-except-diff-v1
     (display-buffer
      buffer (if (with-current-buffer buffer
                   (derived-mode-p 'magit-diff-mode 'magit-process-mode))
                 '(display-buffer-below-selected)
               '(display-buffer-same-window))))
   "Open in same window or (when secondary) split at bottom")
  (magit-diff-refine-hunk t "Show fine differences (word-granularity) for current hunk only"))

Forge

The forge package allows us to interact with different forges like GitHub, GitLab and more from the comfort of Emacs. Updating this while in the Uber and happy to think of me shipping PRs without having to context switch between Emacs and browser and dicking around with a shitty touchpad on Linux (which is the pain of the day for me these days 😭).

;; https://github.com/magit/forge
(use-package forge
  :straight (forge :type git
                   :host github
                   :repo "magit/forge")
  :after magit)

Consult Forge Getting Started for instructions.

Secret management

Forge uses ghub to access the APIs of different forges. Consult Getting Started in case you want to know how under-the-hood forge access happens.

Effectively we need to define the following access details:

Forgemachineuser
GitHubapi.github.comvidbina^EMACS_PACKAGE

https://magit.vc/manual/forge/Token-Creation.html#Token-Creation

In case you rely on auth-source through pass to retrieve passwords/secrets, you will need to create the following pass entry for forge to work:

pass edit api.github.com/vidbina^forge

Fix incompatible recipes issues

⛔ Warning (straight): Packages “magit-section” and “magit” have incompatible recipes (:branch cannot be both nil and “main”) ⛔ Warning (straight): Packages “git-commit” and “magit-section” have incompatible recipes (:branch cannot be both “main” and nil)

Diff-hl: Diff highlighting in the left gutter of a buffer

The diff-hl package allows us to display which lines are added or removed by either providing red or green regions in the margin or fringe.

I’ve been using diff-hl in margin mode for a while, but with the latest updates [2023-07-03 Mon], I disabled margin mode in exchange for fringe mode where we use the more consended representation of showing the diff highlights in the fringe (and thus using less horizontal real-estate). As I don’t use Emacs in the terminal, I don’t care about diff-hl not being able to show up in the terminal when we use it in fringe mode but YMMV. 🤷🏿‍♂️

;; https://github.com/dgutov/diff-hl
(use-package diff-hl
  :straight (diff-hl :type git
                     :host github
                     :repo "dgutov/diff-hl")
  :hook
  (after-init . global-diff-hl-mode)
  (magit-pre-refresh . diff-hl-magit-pre-refresh)
  (magit-post-refresh . diff-hl-magit-post-refresh)

  :custom
  (diff-hl-margin-mode nil "Use the fringe"))

Navigation

Deft

;; https://github.com/jrblevin/deft
(use-package deft
  :straight (deft :type git
                  :host github
                  :repo "jrblevin/deft")
  :after org
  :bind
  ("C-c n d" . deft)
  :custom
  (deft-directory "~/org")
  (deft-extensions '("md" "org"))
  (deft-recursive t)
  (deft-strip-summary-regexp
   (concat "\\("
           "[\n\t]" ;; blank
           "\\|^#\\+[[:alpha:]_]+:.*$" ;; org-mode metadata
           "\\)"))
  (deft-use-filename-as-title t)
  (deft-use-filter-string-for-filename t))

Neotree

In order to have a vim NERDTree-like experience where we get to explore directory structures in a narrow sidebar, we install neotree.

;; https://github.com/jaypei/emacs-neotree
(use-package neotree
  :straight (neotree :type git
                     :host github
                     :repo "jaypei/emacs-neotree")
  :custom
  (neo-theme (if (display-graphic-p) 'icons 'arrow)))

Dirvish

Use Dirvish to supercharge your dired experience. It is closer to offering a ranger-like experience inside of Emacs.

;; https://github.com/alexluigit/dirvish
(use-package dirvish
  :straight (dirvish :type git
                     :host github
                     :repo "alexluigit/dirvish")
  :init
  (dirvish-override-dired-mode)
  :custom
  <<dirvish-custom>>
  :config
  <<dirvish-config>>
  :bind ; Bind `dirvish|dirvish-side|dirvish-dwim' as you see fit
  (("C-c f" . dirvish-fd)
   :map dirvish-mode-map ; Dirvish inherits `dired-mode-map'
   ("a"   . dirvish-quick-access)
   ("f"   . dirvish-file-info-menu)
   ("y"   . dirvish-yank-menu)
   ("N"   . dirvish-narrow)
   ("^"   . dirvish-history-last)
   ("h"   . dirvish-history-jump) ; remapped `describe-mode'
   ("s"   . dirvish-quicksort)    ; remapped `dired-sort-toggle-or-edit'
   ("v"   . dirvish-vc-menu)      ; remapped `dired-view-file'
   ("TAB" . dirvish-subtree-toggle)
   ("M-f" . dirvish-history-go-forward)
   ("M-b" . dirvish-history-go-backward)
   ("M-l" . dirvish-ls-switches-menu)
   ("M-m" . dirvish-mark-menu)
   ("M-t" . dirvish-layout-toggle)
   ("M-s" . dirvish-setup-menu)
   ("M-e" . dirvish-emerge-menu)
   ("M-j" . dirvish-fd-jump)))

Set quick access entries

(dirvish-quick-access-entries ; It's a custom option, `setq' won't work
 '(("h" "~/"                          "Home")
   ("d" "~/Downloads/"                "Downloads")
   ("m" "/mnt/"                       "Drives")
   ("t" "~/.local/share/Trash/files/" "TrashCan")))

Format modeline

;; (dirvish-peek-mode) ; Preview files in minibuffer
;; (dirvish-side-follow-mode) ; similar to `treemacs-follow-mode'
(setq dirvish-mode-line-format
      '(:left (sort symlink) :right (omit yank index)))
(setq dirvish-attributes
      '(all-the-icons file-time file-size collapse subtree-state vc-state git-msg))
;;(setq delete-by-moving-to-trash t)
(setq dired-listing-switches
      "-l --almost-all --human-readable --group-directories-first --no-group")

Bindings

Completion

Function completing-read-default is triggered to resolve completions. Observe the following example that fires up whichever completion framework is actively configured:

(completing-read-default "Pick one 🤷🏿‍♂️: "
                         (list "Blue 🔵 pill 💊"
                               "Red 🔴 pill 💊"))

The help page for the abovementioned function will indicate which completion framework is active.

💡 TIP: You will be able to tell in the help pages which advice is associated to the function thus allowing you to determine which functions are actually triggered.

<<orderless>> Orderless

The orderless package provides more generous completion resolution by permitting us to:

  1. provide partial phrases e.g.: “o i d” to filter for “org-indent-drawer” and
  2. enter these parts in any order (hence orderless) e.g.: “drawer org” to filter for “org-indent-drawer”.
(use-package orderless
  :straight (orderless :type git
                       :host github
                       :repo "oantolin/orderless")
  <<orderless-ivy>>
  :custom
  (completion-styles '(orderless)))

The following note should help us remember to uncomment the Ivy integration when we are using Swiper.

;; NOTE: Load Orderless after Swiper when using the Ivy integration

Marginalia

<a href=”https://github.com/minad/marginalia “>Marginalia annotates entries in a completion buffer with additional context.

;; Enable richer annotations using the Marginalia package
(use-package marginalia
  :straight (marginalia :type git
                        :host github
                        :repo "minad/marginalia")
  ;; Either bind `marginalia-cycle` globally or only in the minibuffer
  :bind (("M-A" . marginalia-cycle)
         :map minibuffer-local-map
         ("M-A" . marginalia-cycle))

  ;; The :init configuration is always executed (Not lazy!)
  :init

  ;; Must be in the :init section of use-package such that the mode gets
  ;; enabled right away. Note that this forces loading the package.
  (marginalia-mode))

Consult

Consult provides enchancements to completion systems based around the standard Emacs completing-read API.

;; https://github.com/minad/consult
(use-package consult
  :straight (consult :type git
                     :host github
                     :repo "minad/consult")
  :bind
  (;; bindings from https://github.com/minad/consult#use-package-example
   <<consult-bindings>>
   )

  :config
  ;; Use `consult-completion-in-region' if Vertico is enabled.
  ;; Otherwise use the default `completion--in-region' function.
  (setq completion-in-region-function
        (lambda (&rest args)
          (apply (if vertico-mode
                     #'consult-completion-in-region
                   #'completion--in-region)
                 args))))

See the following example for Consult’s multiple selection:

(consult-completing-read-multiple "Pick one 🤷🏿‍♂️: "
                                  (list "Blue 🔵 pill 💊"
                                        "Red 🔴 pill 💊"))

Bindings

From the example configuration.

;; C-c bindings (mode-specific-map)
("C-c h" . consult-history)
("C-c m" . consult-mode-command)
("C-c k" . consult-kmacro)
;; C-x bindings (ctl-x-map)
("C-x M-:" . consult-complex-command)     ; orig. repeat-complex-command
("C-x b"   . consult-buffer)              ; orig. switch-to-buffer
("C-x 4 b" . consult-buffer-other-window) ; orig. switch-to-buffer-other-window
("C-x 5 b" . consult-buffer-other-frame)  ; orig. switch-to-buffer-other-frame
("C-x r b" . consult-bookmark)            ; orig. bookmark-jump
("C-x p b" . consult-project-buffer)      ; orig. project-switch-to-buffer
;; Custom M-# bindings for fast register access
("M-#"   . consult-register-load)
("M-'"   . consult-register-store)        ; orig. abbrev-prefix-mark (unrelated)
("C-M-#" . consult-register)
;; Other custom bindings
("M-y"      . consult-yank-pop)           ; orig. yank-pop
("<help> a" . consult-apropos)            ; orig. apropos-command
;; M-g bindings (goto-map)
("M-g e"   . consult-compile-error)
("M-g f"   . consult-flymake)             ; Alternative: consult-flycheck
("M-g g"   . consult-goto-line)           ; orig. goto-line
("M-g M-g" . consult-goto-line)           ; orig. goto-line
("M-g o"   . consult-outline)             ; Alternative: consult-org-heading
("M-g m"   . consult-mark)
("M-g k"   . consult-global-mark)
("M-g i"   . consult-imenu)
("M-g I"   . consult-imenu-multi)
;; M-s bindings (search-map)
("M-s d" . consult-find)
("M-s D" . consult-locate)
("M-s g" . consult-grep)
("M-s G" . consult-git-grep)
("M-s r" . consult-ripgrep)
("M-s l" . consult-line)
("M-s L" . consult-line-multi)
("M-s m" . consult-multi-occur)
("M-s k" . consult-keep-lines)
("M-s u" . consult-focus-lines)
;; Isearch integration
("M-s e" . consult-isearch-history)
:map isearch-mode-map
("M-e"   . consult-isearch-history)       ; orig. isearch-edit-string
("M-s e" . consult-isearch-history)       ; orig. isearch-edit-string
("M-s l" . consult-line)                  ; needed by consult-line to detect isearch
("M-s L" . consult-line-multi)            ; needed by consult-line to detect isearch
;; Minibuffer history
:map minibuffer-local-map
("M-s" . consult-history)                 ; orig. next-matching-history-element
("M-r" . consult-history)                 ; orig. previous-matching-history-element

<<vertico>> Vertico

The vertico package provides a lighter completion solution when compared to Helm or Ivy.

;; https://github.com/minad/vertico
(use-package vertico
  :straight (vertico :type git
                     :host github
                     :repo "minad/vertico")
  :init
  (vertico-mode)

  :config
  )

;; Persist history over Emacs restarts. Vertico sorts by history position.
(use-package savehist
  :straight (:type built-in)
  :init
  (savehist-mode))

;; A few more useful configurations...
(use-package emacs
  :straight (:type built-in)
  :init
  ;; Add prompt indicator to `completing-read-multiple'.
  ;; Alternatively try `consult-completing-read-multiple'.
  (defun crm-indicator (args)
    (cons (concat "[CRM] " (car args)) (cdr args)))
  (advice-add #'completing-read-multiple :filter-args #'crm-indicator)

  ;; Do not allow the cursor in the minibuffer prompt
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))

  ;; Emacs 28: Hide commands in M-x which do not work in the current mode.
  ;; Vertico commands are hidden in normal buffers.
  (setq read-extended-command-predicate
        #'command-completion-default-include-p)

  ;; Enable recursive minibuffers
  (setq enable-recursive-minibuffers t)

  :hook (minibuffer-setup . cursor-intangible-mode))

💡 Remember that non-existing options can be entered using M RET instead of RET (which is convenient when trying to enter options in finders).

Which-key: Show key bindings next to command listing in pop-up

The which-key package annotes the command listing with the key bindings for the shown commands.

images/screenshot-which-key-dark.png

images/screenshot-which-key-light.png

;; https://github.com/justbur/emacs-which-key
(use-package which-key
  :straight (which-key :type git
                       :host github
                       :repo "justbur/emacs-which-key")
  :delight
  :config
  (which-key-mode))

Terminals and Shells

vterm

(use-package vterm
  :straight (:type built-in)
  :after evil
  :init (evil-collection-vterm-setup)
  :hook
  <<vterm-hooks>>
  :config
  <<vterm-config>>)

delete word in vterm prompt changes case

Just type ab, navigate to beginning of word and enter dw to see what happens.

M-S-e triggers eval prompt

Produces an execute: _ prompt where I suppose we get to enter vterm-related shell commands for execution. What is the deal here.

How to enable ffap find-file-at-point within vterm

Using find-file-at-point inside of vterm does not work atm even though ffap does seem dired-aware.

In dired it does seem to work in a manner that suggests that ffap is either dired-aware or dired overrides ffap.

Somehow the ffap function didn’t seem to be overriden in dired, so it seems that the functionality is ffap-native. Looked at dired-find-file to get an idea of a possible way to approach this.

Reading find#Top for more info.

(define-key vterm-mode-map (kbd "C-x C-f") 'vidbina/ffap-vterm-in-persp-mode)

Set magit-respositories-directories for magit dir awareness

Make magit aware of the correct directory when invoked from a vterm.

(vterm-mode . (lambda ()
                (message "HOOK FIRED 2")
                `(let ((target ,(list (cons (vidbina/get-likely-current-directory) 2))))
                   (message "⚠️ Setting %s" target)
                   (customize-set-value 'magit-repository-directories target "Set through vidbina/get-likely-current-directory"))))

Magit status magic-status in vterm

Running magic-status in vterm doesn’t work because variable magit-repository-directory.

(message magit-repository-directories)

Debug evil-collection-vterm-delete

In vterm:

  1. Enter a word after the prompt
  2. selecting that word
  3. Enter dw evil binding (to delete word)

Result: triggers a case change.

Comms

request

https://github.com/tkf/emacs-request

;; https://github.com/tkf/emacs-request
(use-package request
  :straight (request :type git
              :host github
              :repo "tkf/emacs-request"))

PDF

PDF-Tools

(use-package pdf-tools
  :straight (:type built-in)
  :config
  (require 'pdf-occur)
  (pdf-tools-install nil t nil nil)
  (setq-default pdf-view-display-size 'fit-width))

reMarkable

I use the reMarkable 2 as my paper/PDF reading on-the-go. The following helpers allow me to connect to my reMarkable over a USB connection and upload files into my device.

(defun vidbina/upload-pdf-to-remarkable (file)
  (message "📚 reMarkable: Attempt to upload %s" file)
  (request "http://10.11.99.1/upload"
    :headers '(("Origin: http://10.11.99.1'")
               ("Accept" . "*/*")
               ("Referer" . "http://10.11.99.1/")
               ("Connection" . "keep-alive"))
    :files `(("file" . ,file))))

(defun vidbina/from-buffer-upload-pdf-to-remarkable ()
  (interactive)
  (vidbina/upload-pdf-to-remarkable buffer-file-name))

;; Write function to upload file at point to a directory obtained through prompt
(defun vidbina/from-dired-upload-pdf-to-remarkable ()
  (interactive)
  (let* ((file (dired-get-file-for-visit))
         ;; (cmd-long (format "curl \'http://10.11.99.1/upload\' -H 'Origin: http://10.11.99.1' -H 'Accept: */*' -H 'Referer: http://10.11.99.1/' -H 'Connection: keep-alive' -F \"file=@%s;filename=%s;type=application/pdf\"" (file-name-nondirectory file) (file-name-nondirectory file)))
         ;;(cmd-short (format "curl -v -F filename=%s -F upload=@%s http://10.11.99.1/upload" (file-name-nondirectory file) (file-name-nondirectory file)))
         ;;(cmd cmd-short)
         )
    (message (format "Looking at file %s" file))
    ;; (message (format "Will run: %s" cmd)) ;
    ;; (message (format "Quoted: %s" (combine-and-quote-strings (list cmd))))

    ;;(with-temp-buffer
    ;;  (cd (file-name-directory file))
    ;;  (shell-command cmd))

    (vidbina/upload-pdf-to-remarkable file)))

Fix undefined request issue

Project Management

In order to manage projects more conveniently, one can opt for a variety of project management packages. In this section we configure and explain some of the options that I’ve relied on over time.

project.el

First and foremost, project.el (bundled with Emacs) provides some facilities to switch between projects, explore project trees and execute commands (among other features). The project.el bindings are mapped to C-x p.

The following links provide some context that can be helpful in helping inform your decision to learn project.el or projectile (or its derivatives):

Projectile

Projectile simplifies working by projects by providing some bindings that infer their behavior from a project-type. This means that we can remember single bindings expore our project trees as well as triggering project lifecycle commands such as configure, compile and run test, and use these generalizations across projects – allowing ourselves to forget some project-specific details. 😌

;; https://github.com/bbatsov/projectile/
(use-package projectile
  :straight (projectile :type git
                        :host github
                        :repo "bbatsov/projectile")
  :custom
  (projectile-mode-line-prefix "🗄️")
  :hook (after-init . projectile-mode)
  :bind (:map projectile-mode-map
              ("C-x p" . projectile-command-map)))

We configure Projectile by

  1. most generally, defining new project types or
  2. more specifically, populating the .dir-locals.el file with our needed Projectile configuration or project settings.

We use C-x p as the binding prefix projectile deciding to overide the project.el bindings 🙊:

  • C-x p P to trigger a test command using (projectile-test-project ARG)
  • C-x p L to trigger a test command using (projectile-install-project ARG)
  • C-x p ! to run a one-off shell command using (projectile-run-shell-command-in-root)
  • C-x p x s run a shell (projectile-run-shell) (will jump to already running shell unless prefixed)

Defining Projectile lifecycle commands dir-locals.el

Look at projectile-cache-current-file on tips to implementing file-specific Projectile commands.

The following snippet is a rough example of a Projectile lifecycle command that performs an operation on the currently open file.

((nil . ((projectile-project-test-cmd . (lambda ()
                                          (shell-command (format "exercism submit %S" (file-truename (buffer-file-name))))
                                          (message "Ran test"))))))

For some reason, changing the .dir-locals.el file requires a reset of the corresponding map which, in the case above, happens to be the projectile-test-cmd-map. This hashmap can be reset by navigating to the source where is is defined and reevaluating the defining sexpr.

<<perspective>> Perspective

The Perspective package provides some conveniences to manage different workspaces. I use perspectives to keep buffers and layouts isolated between different contexts e.g.: sometimes projects, sometimes features, sometimes tasks (e.g.: wedding planning notes and emails, 1-on-1 work-related notes and details, notes and buffers on a particular research topic, etc.).

;; https://github.com/nex3/perspective-el
(use-package perspective
  :straight (perspective :type git
                         :host github
                         :repo "nex3/perspective-el")
  :bind (("C-x k" . persp-kill-buffer*)
         ("C-x b" . persp-switch-to-buffer))
  :custom
  (persp-mode-prefix-key (kbd "C-c p") "same as persp-mode")
  (persp-modestring-short t)
  (persp-state-default-file "~/.emacs.d/perspective")
  (persp-show-modestring 'header)
  :config
  (message "Configuring ‘perspective’")
  <<perspective-config>>
  :init
  (persp-mode))

Secrets management with auth-source

(use-package auth-source
  :straight (:type built-in)

  :config
  <<auth-source-config>>)

Using the Linux password store (pass) with auth-source

I use pass to manage passwords on my system. The following configuration, demonstrates how to set up the auth-source package to use the password-store backend. You can confirm the backend by verifying that the auth-sources customizable variable is set to (password-store).

(auth-source-pass-enable)

The following is an example of how we access a password in a password store keystore.

(auth-source-pass-get 'secret "domain.tld/handle/password-YYYYMMDD")

Use the ideas in this section to configure your private Emacs config to setup all of the secrets, passwords and other tokens that you want to load from your password store.

Browser

Atomic Chrome

;; https://github.com/alpha22jp/atomic-chrome
(use-package atomic-chrome
  :straight (atomic-chrome :type git
                           :host github
                           :repo "alpha22jp/atomic-chrome"))

<<mail>> Mail

For mail, there are a couple of options within Emacs. First, one needs to understand that a mail user agent (MUA) is a tool used to compose and read messages and a mail transfer agent (MTA) is a tool to send messages.

MUA (Mail User Agent)

You will find the following MTA options in Emacs:

  • message-user-agent, typically the default
  • sendmail-user-agent
  • mh-e-user-agent
  • gnus-user-agent
  • mu4e-user-agent, in case you’re using mu4e

Read Mail Composition Methods (Emacs Manual) for more information on how to compose mail in Emacs.

mu4e

mu4e is a Maildir-friendly mail client that uses mu as a backend.

;; https://www.djcbsoftware.nl/code/mu/mu4e.html
(use-package mu4e
  :after (:all
          <<mu4e-after>>)
  :straight (:type built-in)
  :demand t
  :bind (("C-c M 4" . mu4e))
  :hook (
         <<mu4e-hooks>>)
  :config
  <<mu4e-config>>
  :custom
  <<mu4e-custom>>)

For convenience, remember to prefix the update command by entering it as C-u u in a mu4e-main buffer or by entering C-u C-c C-u from the mu4e-headers buffer such that the update commands run in the background.

We will load mu4e after the message.el and sendmail.el packages are loaded.

message
sendmail

Customizations

We set mu4e as the MUA:

(mail-user-agent 'mu4e-user-agent "Set mu4e a default MUA")
Compose

We enable format=flowed to allow mail viewers to self-determine a suitable wrapping strategy. See a HN thread on Use plaintext email. This choice isn’t universally supported as you may find in the Mailing List Etiquette on OpenStack.

(mu4e-compose-format-flowed t "Compose messages as format=flowed")

Also remember to bottom-post. This is something that I have to internalize myself.

Send Behavior

The pre-context mu4e setup, by default expects a sent folder. By changing the sent messages behavior, we can avoid and then set the behavior again when we context switch.

(mu4e-sent-messages-behavior 'delete "Switch this behavior to 'sent within the appropriate contexts where directory mu4e-sent-folder is correctly set")
Confirmation on quiting mu4e
('mu4e-confirm-quit nil "Stop asking to quit, it bugs me out")
Attaching files through Dired

As outlined in the mu4e appendix, we enable the dired-mode-hook (see Dired) that enables us to use C-c RET C-a to attach files to new or existing mu4e emails.

;; https://www.djcbsoftware.nl/code/mu/mu4e/Dired.html
(dired-mode . turn-on-gnus-dired-mode)

Require gnus-dired:

;; https://www.djcbsoftware.nl/code/mu/mu4e/Attaching-files-with-dired.html
(require 'gnus-dired)

The original gnus-dired’s gnus-dired-mail-buffers returns

  1. all message buffers in case gnus-dired-mail-mode is either message-user-agent or gnus-user-agent (unlikely to be the case for me since I’m using mu4e or notmuch) or
  2. a filtered list of buffers that have mail-mode as their major mode.

We override it to return a list of buffers that derive message-mode.

;; make the `gnus-dired-mail-buffers' function also work on
;; message-mode derived modes, such as mu4e-compose-mode
(defun gnus-dired-mail-buffers ()
  "Return a list of active message buffers."
  (let (buffers)
    (save-current-buffer
      (dolist (buffer (buffer-list t))
        (set-buffer buffer)
        (when (and (derived-mode-p 'message-mode)
                   (null message-sent-message-via))
          (push (buffer-name buffer) buffers))))
    (nreverse buffers)))

We set our preferred mail composition package:

(gnus-dired-mail-mode 'mu4e-user-agent)
Use fancy characters

Disabled until we have some font-issues resolved.

(mu4e-use-fancy-chars nil "Use fancy unicode characters for mu4e marks")
Headers
(mu4e-headers-fields '((:flags . 6) (:human-date . 12) (:from . 20) (:subject)))
(mu4e-headers-date-format "%F")

Disable saving of drafts

  • State “PROTOTYPE” from “TODO” [2023-01-31 Tue 07:16]
    Introducing this to minimize the draft files noise

Based on https://emacs.stackexchange.com/a/56334/37975

(add-hook 'mu4e-compose-mode-hook #'(lambda () (auto-save-mode -1)))
(mu4e-sent-messages-behavior 'delete)
Background

https://emacs.stackexchange.com/a/24430

(defun draft-auto-save-buffer-name-handler (operation &rest args)
  "for `make-auto-save-file-name' set '.' in front of the file name; do nothing for other operations"  
  (if
      (and buffer-file-name (eq operation 'make-auto-save-file-name))
      (concat (file-name-directory buffer-file-name)
              "."
              (file-name-nondirectory buffer-file-name))
    (let ((inhibit-file-name-handlers
           (cons 'draft-auto-save-buffer-name-handler
                 (and (eq inhibit-file-name-operation operation)
                      inhibit-file-name-handlers)))
          (inhibit-file-name-operation operation))
      (apply operation args))))

(add-to-list 'file-name-handler-alist '("Drafts/cur/" . draft-auto-save-buffer-name-handler))

Contexts

In mu4e we can use contexts to manage the different “contexts” in which we write and read email.

We configure mu4e to try to detect a context based using the match or :

(mu4e-context-policy 'ask-if-none)

During composing we do the same, just ask-if-none:

(mu4e-compose-context-policy 'ask-if-none)

Switching contexts during e-mail drafting can be achieved with the C-c ; binding. Just remember that.

Example

The example contexts in the mu4e documentation should be sufficiently detailed to provide you insight into how to write your own. We can write contexts into our configuration as follows:

(setq mu4e-contexts
      `( ,(make-mu4e-context
           :name "Sample"
           :enter-func (lambda () (mu4e-message "Into SAMPLE mu4e context"))
           :leave-func (lambda () (mu4e-message "Out of SAMPLE mu4e context"))
           :vars '(( user-mail-address . "[email protected]")))))

The better approach, however is to write contexts through a personal file that is not tracked in this public repository.

Indexing

I started the config by turning indexing off indexing in order to handle this through a systemd service instead and unburden Emacs but mu4e doesn’t play well with not managing indexing itself so we’re going to let mu4e handle its index.

(mu4e-index-update-in-background t "Index in background")
Debug Mode
(mu4e-mu-debug t "Run mu in debug mode")
Strategies

I define two indexing strategies below of which you are only to keep one uncommented at a time.

Behavior: Lean Indexing

The mu4e manual discusses how to speed up indexing which is something that we deliberately turn on because indexing takes a long time my machine and effectively locks me out of any mu4e use while ongoing.

(mu4e-index-cleanup nil)
(mu4e-index-lazy-check t)
Define heavy indexing job post hibernate

Solve the “locked out of mu4e because I’m indexing” problem by defining a heavier indexing job before or after hibernate to just make sure a thorough re-indexing happens before every session. Perhaps, before hibernate is better because when returning from suspend, I can imagine that there are plenty of things that I quickly want to get done, while I would be forgiving if I tell the machine to hibernate but it still takes it sweet time to continue a indexing chore before properly suspending.

Updating
Retrieve outside of mu4e

I have configured mbsync to retrieve mail in my personal configuration

(mu4e-get-mail-command "true" "Noop during retrieval and just handle indexing")
(mu4e-update-interval 300 "Auto index every 5 minutes")

MTA (Mail Transport Agent)

We configure sendmail to serve as our MTA (Mail Transport Agent) of choice to handle the sending of email. For sending (shipping) mail in Emacs, we have the following options:

  • sendmail
  • feedmail, which does some “massaging” (think transformations) on outgoing messages
  • smtpmail, which directly delivers mail to SMTP mail server from an Emacs buffer
  • mailclient, which triggers the system’s mail client to continue editing of the message before shipping it off

Background

Some of the functions that are relevant:

  • sendmail-send-it
  • feedmail-send-it
  • smtpmail-send-it
  • mailclient-send-it
  • *message-smtpmail-send-it* calls smtpmail-send-it after evaluating message-send-mail-hook but is obsolete and message-use-send-mail-function is recommended instead which does almost the same (running message-send-mail-hook and then firing send-mail-function)

Since we are using sendmail (or something sendmail-compatible like msmtp), we will be customizing some sendmail.el and message.el variables to refect our setup details.

Piecing the ecosystem of Emacs function and variables together is a lot easier when there is a visual overview, so here goes.

images/sendmail-func-callgraph.png

;; message.el
(append (list (point-min) (point-max)
              sendmail-program
              nil errbuf nil "-oi")
        message-sendmail-extra-arguments
        ;; Always specify who from,
        ;; since some systems have broken sendmails.
        ;; But some systems are more broken with -f, so
        ;; we'll let users override this.
        (and (null message-sendmail-f-is-evil)
             (list "-f" (message-sendmail-envelope-from)))
        ;; These mean "report errors by mail"
        ;; and "deliver in background".
        (if (null message-interactive) '("-oem" "-odb"))
        ;; Get the addresses from the message
        ;; unless this is a resend.
        ;; We must not do that for a resend
        ;; because we would find the original addresses.
        ;; For a resend, include the specific addresses.
        (if resend-to-addresses (list resend-to-addresses) '("-t")))

<<sendmail.el>> sendmail.el

I have a few sendmail settings that I want loaded, but we bumped into errors here about sendmail-program being undefined that suggests that sendmail isn’t fully autoloaded at this point in the code and instead of moveing everything to a later point, we will use with-eval-after-load instead to trigger our customizations after package loading.

(use-package sendmail
  :straight (:type built-in)
  :custom
  <<sendmail-custom>>)

We default by setting the mail functions to their blocking variety:

(send-mail-function 'smtpmail-send-it "Default to block")

Enable debugging mode

For visibility of our SMTP traffic, we enable debugging of SMTP output to a buffer named *trace of SMTP session to <SOMEWHERE>*.

(smtpmail-debug-info t "Enable debugging")

Envelope From

⚠️ This section may be moot as we will be using message.el facilities (identifiable by message--prefixed function and variable names) for the sending of mail and message.el does not seemt to rely on the sendmail.el-specific from-logic but reimplements its own. See the summary detailing some differences in how message.el and sendmail.el call the sendmail program.

Check out the definition of function sendmail-send-it for a glimpse into how sendmail is being called. With variable mail-specify-envelope-from set to a non-=nil= value, this function calls (mail-envelope-from), which always returns the From: header of the e-mail when variable mail-envelope-from is set to 'header, or defaults to user-mail-address.

In our case, instead of setting from throught the -f argument we can use the -a account CLI option to specify the context for sendmail. We still have to provide some logic to make this work but disabling sendmail from accidentally setting -f envelope-from when calling sendmail is a good starting point to avoid some embarassing mishaps.

(mail-specify-envelope-from nil "Don't try to be smart, use user-mail-address")
(mail-envelope-from nil "Don't try to be smart, use user-mail-address")

💡 My msmtp configuration does not define a default account in order to make account management very explicit. I don’t accidentally want emails leaving my machine from a wrong SMTP server because msmtp assumed a default account.

<<message.el>> message.el

(use-package message
  :straight (:type built-in)
  :custom
  <<message-custom>>)

We set the mail directory:

(message-directory "~/mail/")

Set MTA

The message.el package provides a more general interfaces for message sending, so we will configure message.el to use sendmail as out MTA of choice:

(message-send-mail-function 'message-send-mail-with-sendmail "Use sendmail as our MTA")

Envelope From

(message-sendmail-f-is-evil t "Avoid setting -f (--from) when calling sendmail")
(message-sendmail-envelope-from 'header "Use From: header")

Misc

(message-kill-buffer-on-exit t "Kill a buffer once a message is sent")

Finale

To keep our init as general as possible we store private information and language configurations in separate files since these are inherently personal concerns. This configuration will try to load lang.el and personal.el if these exist.

(message "💥 Debug on error is %s" debug-on-error)

(load "~/.emacs.d/lang.el" t)
(load "~/.emacs.d/personal.el" t)

Customizations

Furthermore we load customization since some configurations and changes to our Emacs setup will be persisted through the customization system.

;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Saving-Customizations.html
(setq custom-file "~/.emacs.d/custom.el")
(load custom-file t)

Personal Details

💡 You can copy the content in this section to your own personal.org file in this directory and configure all the :tangle arguments to output to personal.el to cook up your own personal part of your configuration through literal programming. Remember that you can tangle an Org-file into the resulting code with the (org-babel-tangle) command (mapped to C-c C-v t by default).

Populate a personal.el file which defines your name, your e-mail details and some other very personal details. 📛 Personal theme customizations or keybindings are good fits for the file:personal.el as well.

Use the following snippet as an example of a configuration that may work.

💡 Tangling this section/file should produce personal-example.el that you can use as a reference for your own file.

(setq user-full-name "David Asabina"
      inhibit-startup-screen t
      frame-title-format '(multiple-frames "%b" ("" "Emacs :: %b")))

Org-capture Templates

(setq org-capture-templates
      (list
       <<my-org-capture-templates>>
       nil))

Because templates expressed as string literals are difficult to read, debug and edit, we opt for a form that more closely represents the visual form that our templates will take on (i.e.: show real whitespacing for structure). The snippets in this section are tangled into real Org files which are referred to when setting org-capture-template.

⚠️ Please keep in mind that this section tangles into the absolute the relative path templates/ which can wreak havoc on your setup if you already have files in that directory that you will need.

* %^{Title}

Source: %u, %c

%i

Which can be configured using the following template entry:

(list "w" "Default Template" 'entry
      '(file+headline "~/org/protocol/capture.org" "Notes")
      `(file ,(expand-file-name "templates/default.org"))
      :empty-lines 1)

TODOs

* TODO %?

%i

%a
(list "t" "Todo" 'entry
      '(file+headline "~/org/todo.org" "Tasks")
      `(file ,(expand-file-name "templates/todo.org")))

Links

Capture template for a basic Link

For links we define the basic template:

* TODO Read _%:description_

Source: %:annotation%?

Which we map to L:

(list "L" "Link Only" 'entry
      '(file+headline "~/org/protocol/capture.org" "Links")
      `(file ,(expand-file-name "templates/link.org"))
      :empty-lines 2)
Capture template for Link with Text

For links with additional text we define the template:

* TODO Read %^{title}

Source: %:annotation

#+begin_quote
%i
#+end_quote%?

which we map to p:

(list "p" "Link with Selected Text" 'entry
      '(file+headline "~/org/protocol/capture.org" "Links")
      `(file ,(expand-file-name "templates/link-with-text.org"))
      :empty-lines 2)

Email

Citation line

In order to keep things lean, I’ve defined my own citation line that easy enough to parse as opposed to the default line.

(setq message-citation-line-format "On %d.%m.%Y, %f wrote:\n"
      message-citation-line-function #'message-insert-formatted-citation-line)

Mu4e

Contexts

In order to get mail to work for multiple mailboxes you will need to configure mu4e contexts. Refer to the examples in the documentation for some guidance on how to define contexts.

;; TODO fill in the blanks for mu4e-contexts
(setq mu4e-contexts `())

Notmuch

https://notmuchmail.org/emacstips/

(setq notmuch-saved-searches
      '((:name "inbox" :query "tag:inbox" :key "i")
        (:name "unread" :query "tag:unread" :key "u")
        (:name "flagged" :query "tag:flagged" :key "f")
        (:name "sent" :query "tag:sent" :key "t")
        (:name "drafts" :query "tag:draft" :key "d")
        (:name "all mail" :query "*" :key "a")))

Personal Helpers

Here be dragons! 🐉 This is my personal collection of helpers that I use for little things like switching themes, managing wrapping inside of buffers, managing opening of URL’s and more junk. I will not explain these as these are simple enough and I’m not expecting me needing to explain this to myself or others (you likely will want to write your own).

Themes

(defcustom vidbina/theme-should-be-dark nil
  "Non-nil means that the theme should be dark"
  :type 'boolean
  :group 'display)

(defun vidbina/theme-switch-update-cursor-styles ()
  "Update colors of cursors for new theme"
  (interactive)
  (message "🌕 Setting evil cursors with modus-themes colors")
  (let ((my-cursor-color (modus-themes-get-color-value 'fg-main))
        (my-intense-cursor-color (modus-themes-get-color-value 'red-intense))
        (my-alt-cursor-color (modus-themes-get-color-value 'fg-alt)))
    (setq evil-emacs-state-cursor `(box ,my-intense-cursor-color))
    (setq evil-insert-state-cursor `((bar . 5) ,my-cursor-color))
    (setq evil-visual-state-cursor `((hbar . 5) ,my-cursor-color))
    (setq evil-motion-state-cursor `((bar . 5) ,my-cursor-color))
    (setq evil-normal-state-cursor `(box ,my-cursor-color))))

(defun vidbina/theme-switch-to-choice ()
  "Switch to the theme of choice"
  (message "🦋 Switching to vidbina's theme")
  (if vidbina/theme-should-be-dark
      (vidbina/theme-switch-to-dark)
    (vidbina/theme-switch-to-light)))

(defun vidbina/theme-switch-to-dark ()
  "Switch to the dark theme"
  (interactive)
  (setq zoom-window-mode-line-color "DodgerBlue4")
  (load-theme 'modus-vivendi :no-confirm)
  (vidbina/theme-switch-update-cursor-styles)
  ;; call commands
  ;; hsetroot -solid '#000000'
  (async-shell-command "hsetroot -solid '#000000'" "*dotfile-helpers*")
  (message "see *dotfile-helpers* for output of wallpaper color set")
  (message "🌑 Theme is dark")
  (customize-save-variable 'vidbina/theme-should-be-dark t))

(defun vidbina/theme-switch-to-light ()
  "Switch to the light theme"
  (interactive)
  (setq zoom-window-mode-line-color "Gold")
  (load-theme 'modus-operandi :no-confirm)
  (vidbina/theme-switch-update-cursor-styles)
  ;; call commands
  (async-shell-command "hsetroot -solid '#ff9800'" "*dotfile-helpers*")
  (message "see *dotfile-helpers* for output of wallpaper color set")
  (message "🌕 Theme is light")
  (customize-save-variable 'vidbina/theme-should-be-dark nil))

(defun vidbina/theme-toggle ()
  "Toggle theme"
  (interactive)
  (if vidbina/theme-should-be-dark
      (vidbina/theme-switch-to-light)
    (vidbina/theme-switch-to-dark)))

(add-hook 'after-init-hook 'vidbina/theme-switch-to-choice)

Org-Export Helpers

(defun vidbina/toggle-local-org-export-use-babel ()
  "Toggle buffer-local org-export-use-babel"
  (interactive)
  (if org-export-use-babel
      (setq-local org-export-use-babel nil)
    (setq-local org-export-use-babel t))
  (message (format "❓ org-export confirm = %s" org-export-use-babel)))

(defun vidbina/toggle-local-org-confirm-babel-evaluate ()
  "Toggle buffer-local org-confirm-babel-evaluate"
  (interactive)
  (if org-confirm-babel-evaluate
      (setq-local org-confirm-babel-evaluate nil)
    (setq-local org-confirm-babel-evaluate t))
  (message (format "☑️ Org Babel confirmation is %s" org-confirm-babel-evaluate)))

Wrapping

(defun vidbina/wrap ()
  "Toggle wrapping using adaptive-wrap-prefix-mode and visual-line-mode"
  (interactive)
  (let ((vidbina/wrap-set
         (lambda (state)
           (progn
             (if state
                 (progn
                   (visual-line-mode +1)
                   (adaptive-wrap-prefix-mode +1))
               (visual-line-mode -1)
               (adaptive-wrap-prefix-mode -1))
             (setq-local vidbina/wrap--state state)
             (message (format "🎁 state=%s wrap -> %s and line -> %s" state adaptive-wrap-prefix-mode visual-line-mode))))))
    (unless (boundp 'vidbina/wrap--state)
      (setq-local vidbina/wrap--state nil))
    (funcall vidbina/wrap-set (not vidbina/wrap--state))))
;; https://stackoverflow.com/questions/12663061/emacs-auto-scrolling-log-buffer
(defun vidbina/tail-buffer ()
  (setq-local window-point-insertion-type t))

Web-Browsing

(defun vidbina/browse-url-xsel (url &optional ignored)
  (shell-command (format "echo \"%s\" | xsel -ib" url)))

(setq browse-url-browser-function 'vidbina/browse-url-xsel)

(defun vidbina/browse-to-current-file ()
  "Open saved HTML file with default browser"
  (progn
    (when (derived-mode-p 'html-mode)
      (progn
        (message (concat "Browse " buffer-file-name))
        (browse-url (file-truename buffer-file-name))))))

(add-hook 'after-save-hook 'vidbina/browse-to-current-file)

Notmuch Inbox Toggler

(defun vidbina/notmuch-toggle-inbox ()
  "toggle inbox tag of message"
  (interactive)
  (if (member "inbox" (notmuch-search-get-tags))
      (notmuch-search-tag (list "-inbox"))
    (notmuch-search-tag (list "+inbox"))))

(evil-collection-define-key 'normal 'notmuch-search-mode-map
  "i" 'vidbina/notmuch-toggle-inbox)

Mail Signature Helpers

(defun vidbina/mail-sig-match (pattern from)
  "Matches From field to a regex"
  (string-match-p pattern from))

(defun vidbina/mail-sig-file (path)
  "Retrieves a signature text by path"
  (format "%s" (with-temp-buffer
                          (insert-file-contents path)
                          (buffer-string))))

(defun vidina/mail-sig ()
  "Returns signature based on From field"
  (let ((from-field (message-field-value "From")))
    (message "Trying out signature for %S" from-field)
    (pcase (or from-field "")
      ((pred (vidbina/mail-sig-match "@example.com"))
       (vidbina/mail-sig-file "/home/example/my-example.sig"))
      (_ (format "Be kind! 🤗")))))

(setq message-signature #'vidbina/mail-sig)

Org-roam UI Navigation

(defcustom vidbina/orui-node-zoom-padding 10
  "Padding to pass to org-roam-ui when navigating with vidbia-org-roam-ui-node-zoom"
  :type 'number
  :group 'display)

(defun vidbina/orui-node-zoom-padding-set ()
  "Set the padding for org-roam-ui-node-zoom"
  (interactive)
  (let ((padding (read-number "🔍:" vidbina/orui-node-zoom-padding)))
    (customize-save-variable 'vidbina/orui-node-zoom-padding padding)))

(defun vidbina/orui-node-zoom ()
  "Zoom to org-roam-ui node with custom padding"
  (interactive)
  (let ((id (org-roam-id-at-point))
        (padding vidbina/orui-node-zoom-padding))
    (org-roam-ui-node-zoom id nil padding)
    (message "🕸️ ORUI zoom 🔍 %s to %s" id padding)))

Helper for Desktop Entries to handle different MIME types

We can configure desktop entries (freedesktop.org) that open URI’s in a new frame using the --create-frame (shorthand -c) argument while also setting the xproperties through --frame-parameters (shorthand -F) that may aid a window manager in positioning the windows correctly. I learned about defining frame parameters first through a StackOverflow thread thread. The following snippet provides a demonstration on how to open a frame through the aforementioned parameters:

emacsclient -F '((name . "Dired"))' --create-frame -a emacs --eval "(let ((path \"/tmp\")) (delete-other-windows-internal) (message (concat \"Dired will open: \" path)) (dired path) path)"

Note that eval has to be a single expression. In the following example we wrap multiple sexps in a progn form to qualify as a single expression:

emacsclient -F '((name . "experiment"))' --create-frame -a emacs --eval "(progn (message \"hi there\") (message \"bye\"))"

Considering how verbose the eval value is and how error prone modifying this may be, we define a helper function for simplicity.

When create-frame is set, we create a new frame with make-frame-command and then clear that frame from any other windows through the delete-other-windows-internal command. This is useful because the creation of a new frame doesn’t always yield a “clean frame” and could therefore be rather noisy (as it may display the multiple windows that were in the previously active frame).

(defun vidbina-mime-handle--open (window-name func target &optional create-frame)
  "Spawn a new frame with the proper qualities"
  ;;(if create-frame (select-frame (make-frame `((name . ,window-name)))))
  (message (concat "MIME handler opening " target))
  (if create-frame (progn
                     (select-frame (make-frame-command))
                     (delete-other-windows-internal)))
  (funcall func target)
  (message "MIME handler opened: \"%s\" in \"%s\"" target window-name))

The helper can be tested through the snippet below and should be less disruptive when the create-frame argument is set since:

  1. it opens up a new frame (leaving existing frames as-is)
  2. reduces the new frame to a single window where only the opened location is presented (for focus)
(vidbina-mime-handle--open "Dired" #'dired "/tmp" 'create-frame)

⚠️ The setting of the create-frame argument may only be necessary when you are not using the --create-frame CLI argument when invoking the emacsclient command.

The defined function can be used inside of a Exec key of a <a href=”https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-0.9.5.html “>Desktop entry i.e.: a .desktop file.

emacsclient -a emacs -F "((name . \"emacs-dired\"))" --eval "(vidbina-mime-handle--open \"Dired\" #'dired \"/tmp\" 'create-frame)"

Specialized User-friendly Helpers

Invoking vidbina-mime-handle--open through an Exec key in a desktop entry gets messy because we’re entering a sexp that references function symbols. Just keeping everything escaped correctly is already a non-trivial problem (for me). It gets even worse when trying to formulate such command invocations inside of a declarative Nix-based configuration where we introduce another level of “escaping” special characters. 😭

At some point it becomes too Inception-esque to reasonably assume that future me will be able to use it with relative ease. Specialized helpers are therefore formulated to greatly reduce the complexity of the information to be entered into a Desktop Entry’s Exec key.

(defun vidbina-mime-handle-open-directory (window-name target &optional create-frame)
  "Open a directory in a new frame"
  (vidbina-mime-handle--open window-name #'dired target create-frame))
(defun vidbina-mime-handle-open-message-in-mu4e (window-name target &optional create-frame)
  "Open a message in a new frame"
  (vidbina-mime-handle--open window-name #'mu4e~compose-mail target create-frame))

Conveniences

Store input to kill-ring

Writing to the kill ring is done on a buffer-basis. Sometimes one just want to yank (in vim-lingo) or kill (in Emacs-lingo) a value to the kill-ring for later reference.

(defun vidbina/kill (object)
  "Yank object"
  (with-temp-buffer
    (insert object)
    (kill-region (point-min) (point-max))))

Store code block into kill-ring

Here is an example text that we can just copy
(defun vidbina/copy-block ()
  "Copies or yanks (vi) the contents of code block at point"
  (interactive)
  (if (org-in-src-block-p)
      (progn (org-babel-mark-block)
             (let ((start-pos (region-beginning))
                   (end-pos (region-end)))
               (copy-region-as-kill start-pos end-pos)))
    (error "Not in code-block")))

Open process buffer below active buffer

(defun vidbina/open-proc-below (proc)
  "Open proc buffer below the current buffer"
  (save-excursion
    (split-window-below)
    (evil-window-down 1)
    (switch-to-buffer (process-buffer proc))
    (evil-window-up 1)))

Remove text properties

(defun vidbina/unpropertize (string)
  "Remove the text properties from a string"
  (let* ((s string)
         (start 0)
         (end (length string)))
    (set-text-properties start end nil s)
    s))

(defalias 'vidbina/depropertize 'vidbina/unpropertize)
;; https://nullprogram.com/blog/2019/12/10/
(put 'vidbina/depropertize 'byte-optimizer 'byte-compile-inline-expand)

Copy VC branch for later use

Sometimes, I just need the current branch name to paste into an email or somewhere else. The M-w binding, mapped to (magit-copy-buffer-revision), typically only provides a rev which isn’t always sufficiently informative and only works within a select few major (magit) modes.

(defun vidbina/magit-branch ()
  "Copy, kill (in Emacs lingo) or yank (in vim lingo) the current branch name"
  (interactive)
  (let ((target (magit-get-current-branch)))
    (vidbina/kill target)
    (message target)))

Misc

Global Keybindings

My special bindings

My special bindings are typically prefixed with C-c v (for vidbina) and also because it didn’t typically collide with other default bindings. 🙈

(global-set-key (kbd "C-c v l") 'vidbina/theme-switch-to-light)
(global-set-key (kbd "C-c v d") 'vidbina/theme-switch-to-dark)
(global-set-key (kbd "C-c v TAB") 'vidbina/wrap)
(global-set-key (kbd "C-c v \\") 'visual-fill-column-mode)
(global-set-key (kbd "C-c v SPC") 'whitespace-mode)
(global-set-key (kbd "C-c v c") 'copilot-complete)
(global-set-key (kbd "C-c v t") 'gptel-menu)
(global-set-key (kbd "C-c v i") 'completion-at-point)
(global-set-key (kbd "C-c v O") 'vidbina/orui-node-zoom-padding-set)
(global-set-key (kbd "C-c v _") 'vidbina/tail-buffer)
(global-set-key (kbd "C-c v .") 'vidbina/orui-node-zoom)
Window zoom helpers
(global-set-key (kbd "C-c v z") 'zoom-window-zoom)
Show Flymake buffer diagnostics
(global-set-key (kbd "C-c v j") 'flymake-show-buffer-diagnostics)

Org bindings

(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "C-c a") 'org-agenda)

Visual Aids

(setq fill-column 1)

(setq whitespace-style '(trailing tabs newline tab-mark newline-mark))

Org Conveniences

;; https://orgmode.org/manual/Handling-Links.html
(setq org-return-follows-link t)

(setq org-log-into-drawer "LOGBOOK")

;; Allow for resizing of images
(setq org-image-actual-width nil)

(setq org-html-head-extra
      "<link rel=\"alternate stylesheet\" type=\"text/css\" href=\"~/org/style.css\" />")
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Position-Info.html

Reload inline images after Org export

;; https://joy.pm/post/2017-09-17-a_graphviz_primer/
(defun my/fix-inline-images ()
  (when org-inline-image-overlays
    (org-redisplay-inline-images)))

(add-hook 'org-babel-after-execute-hook 'my/fix-inline-images)

<<mode-line>> Customize Mode-line

For additional context, one can display mode line or header line elements along the bottom and top of a window respectively.

;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Mode-Line-Variables.html
;; http://emacs-fu.blogspot.com/2011/08/customizing-mode-line.html
(setq-default mode-line-format
              (list "%e"
                    ;; ** when modified
                    ;; -- if not modified
                    ;; %% when read-only
                    ;; %+ read-only but modified
                    mode-line-modified


                    mode-line-frame-identification
                    mode-line-buffer-identification

                    ;; https://evil.readthedocs.io/en/latest/overview.html?highlight=mode-line#modes-and-states
                    ;; <N> normal state
                    ;; <I> insert state
                    ;; <V> visual state
                    ;; <R> replace state
                    ;; <O> operator-pending state
                    ;; <M> motion state
                    ;; <E> emacs state
                    ;; evil-mode-line-tag

                    mode-line-modes
                    (propertize "(%c,%l)%p ")
                    ""))
(message "🕹️ Mode-line set")

You can force update the mode line in the setq doesn’t quite get the job done:

(force-mode-line-update t)

Debug why evil-mode-line-tag in the mode-line is not updating

I stubbed a helper to try to coerce a modeline update since I noticed on [2023-01-24 Tue 20:49] (while enroute to CDMX 🇲🇽) that the mode-line was not updating for the evil mode lighter.

(defun vidbina/refresh-mode-line ()
  (interactive)
  (message "Updating mode-line %s" evil-mode-line-tag)
  (force-mode-line-update t)
  (evil-refresh-mode-line))

In Org and Elisp buffers the mode line is not updating but in the *Messages* buffer, I have observed

Security

Org-crypt

In order to encrypt entries of Org files, the package org-crypt needs to be configured.

(require 'org-crypt)
(org-crypt-use-before-save-magic)

Usage

Usage of org-crypt is as simple as tagging the heading of a section to be encrypted with :crypt:. With the org-crypt-key variable set to nil, symmetric encryption is used. By setting this variable to a string, or by setting the CRYPTKEY property as demonstrated below, we can encrypt against a GPG public key of choice.

* For all eyes

This is nothing special

* For my eyes only :crypt:
:PROPERTIES:
:CRYPTKEY: 0xffffffffffffffffffffffffffffffffffffffff
:END:

This would be encrypted upon safe 😉

* Local File Variables

Disable auto-save since I'm using crypt in this file.

# Local Variables:
# auto-save-default: nil
# End:
Verify if epa-file-encrypt-to works as expected

An alternate approach would be to append the epa-file-encrypt-to variable to the local variables list. The benefit of this is that one can encrypt a file for multiple recipients. I haven’t tested this yet. 🤔

Languages

Populate a lang.el file which defines all of the major-modes and language-related tooling that are relevant to you. In my case I have simply defined a symlink from lang.example.el to lang.el. The literal configuration in this section defines my own languages setup. YMMV! 🤷🏿‍♂️

Natural Language

Spell Checking

Use the built-in ispell for spell checking.

This configuration is derived from the article Setting up spell checking with multiple dictionaries in Emacs (by Alain M. Lafon from 200ok.ch).

Ported use-package configuration

;; https://200ok.ch/posts/2020-08-22_setting_up_spell_checking_with_multiple_dictionaries.html
(use-package ispell
  :straight (:type built-in)
  :custom
  (ispell-program-name "hunspell")
  ;; Configure German, Swiss German, and two variants of English.
  (ispell-dictionary "en_US,de_DE,nl")
  ;; For saving words to the personal dictionary, don't infer it from
  ;; the locale, otherwise it would save to ~/.hunspell_de_DE.
  (ispell-personal-dictionary "~/.hunspell_personal")
  (ispell-personal-dictionary "~/.hunspell_personal")
  :config
  ;; https://www.emacswiki.org/emacs/FlySpell#h5o-14
  (add-to-list 'ispell-skip-region-alist '("^#+BEGIN_SRC" . "^#+END_SRC"))

  ;; ispell-set-spellchecker-params has to be called
  ;; before ispell-hunspell-add-multi-dic will work
  (ispell-set-spellchecker-params)
  (ispell-hunspell-add-multi-dic ispell-dictionary)
  ;; The personal dictionary file has to exist, otherwise hunspell will
  ;; silently not use it.
  (unless (file-exists-p ispell-personal-dictionary)
    (write-region "" nil ispell-personal-dictionary nil 0)))

Markup Languages

Markdown

;; https://jblevins.org/projects/markdown-mode/
(use-package markdown-mode
  :straight (markdown-mode :type git
                           :host github
                           :repo "jrblevin/markdown-mode")
  :commands (markdown-mode gfm-mode)
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode))
  :init
  (setq markdown-command "multimarkdown"))

Helper to convert a markdown file to an Org file. From my private notes, the following command comes in handy:

pandoc -f markdown -t org -o README.org --wrap=preserve README.md
(defun vidbina/markdown-move-to-orgdown ()
  "Convert a markdown file to an orgdown file"
  (interactive)
  (let* ((origin-file-name (buffer-file-name))
         (temp-file-name (file-name-with-extension origin-file-name ".prev.md"))
         (target-file-name (file-name-with-extension origin-file-name ".org")))
    ;; move .md file to .org file
    (copy-file origin-file-name temp-file-name t)
    (magit-file-rename origin-file-name target-file-name)
    (shell-command (format "pandoc -f markdown -t org -o %s --wrap=preserve %s"
                           (shell-quote-argument target-file-name)
                           (shell-quote-argument temp-file-name)))))

HTLM/Webdev

Helper to refactor JSX

(defun vidbina/wrap-wrap-string-to-interpolate ()
  (interactive)
  (let ((pattern "'<,'>s|['\"]\\([ :.a-zA-Z0-9-\\[\\]]+\\)['\"]|{`\\1`}|"))
    (evil-ex pattern)))
<div className="haha">
</div>

<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border border-gray-700 bg-gray-800 text-[0.625rem] font-medium text-red-400 group-hover:text-white">

Serialization Languages/Formats

JSON

JSON-mode provides a major-mode and some keybindings to simplify working with JSON.

;; https://github.com/joshwnj/json-mode
(use-package json-mode
  :straight (json-mode :type git
                       :host github
                       :repo "joshwnj/json-mode"))

Some of the relevant keybindings are:

  • C-c C-f format region or buffer with json-reformat
  • c-c P copy path to object at point to the kill ring

JSON Reformat

JSON Reformat provides convenience helpers to reformat JSON in string or region.

;; https://github.com/gongo/json-reformat
(use-package json-reformat
  :straight (json-reformat :type git
                           :host github
                           :repo "gongo/json-reformat"))

JSON Snatcher: Extract Element Paths within a JSON Structure

JSON Snatcher allows extraction of “addresses” or “paths” to an item within a JSON structure i.e.: snatching.

;; https://github.com/Sterlingg/json-snatcher
(use-package json-snatcher
  :straight (json-snatcher :type git
                           :host github
                           :repo "Sterlingg/json-snatcher"))

Jsonnet

;; https://github.com/tminor/jsonnet-mode
(use-package jsonnet-mode
  :straight (jsonnet-mode :type git
                          :host github
                          :repo "tminor/jsonnet-mode"))

YAML

;; https://github.com/yoshiki/yaml-mode
(use-package yaml-mode
  :straight (yaml-mode :type git
                       :host github
                       :repo "yoshiki/yaml-mode"))

YAML Formatter

yamllint

YAML LSP-support

We install the YAML language server to offer LSP support for YAML files.

nodePackages.yaml-language-server

Viz Languages for graphing, plotting and more

PlantUML

;; https://github.com/skuro/plantuml-mode
(use-package plantuml-mode
  :straight (plantuml-mode :type git
                           :host github
                           :repo "skuro/plantuml-mode")
  :after org
  :config
  ;; https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-dot.html
  (setq org-plantuml-exec-mode 'plantuml)

  (setq plantuml-default-exec-mode 'executable)
  (org-babel-do-load-languages 'org-babel-load-languages
                               (append org-babel-load-languages
                                       '((plantuml . t)))))

Graphviz

;; https://github.com/ppareit/graphviz-dot-mode
(use-package graphviz-dot-mode
  :straight (graphviz-dot-mode :type git
                               :host github
                               :repo "ppareit/graphviz-dot-mode")
  :after org
  :config
  (setq graphviz-dot-indent-width 2)
  (org-babel-do-load-languages 'org-babel-load-languages
                               (append org-babel-load-languages
                                       '((dot . t)))))

Gnuplot

;; https://github.com/emacsorphanage/gnuplot
;; also https://github.com/bruceravel/gnuplot-mode
;; also https://github.com/rudi/gnuplot-el
(use-package gnuplot
  :straight (gnuplot :type git
                     :host github
                     :repo "emacsorphanage/gnuplot")
  :after org
  :config
  (org-babel-do-load-languages 'org-babel-load-languages
                               (append org-babel-load-languages
                                       '((gnuplot . t)))))

Mermaid

https://github.com/abrochard/mermaid-mode

;; https://github.com/abrochard/mermaid-mode
(use-package mermaid-mode
  :straight (mermaid-mode :type git
                          :host github
                          :repo "abrochard/mermaid-mode"))

Enter a dedicated buffer by hovering over any mermaid containing block and enter C-c C-o to open a live editor in a browser.

See Override for mermaid on GitHub

ERD

|o, o|
Zero or one
||, ||
Exactly one
}o, o{
Zero or more (no upper limit)
}|, |{
One or more (no upper limit)
erDiagram
    User ||..o{ Source : has
    User ||..o{ EmailAddress : has
    Source ||..o{ SourceSecret : has
Loading

DSL

The selected nomer is a poor choice if you think about it. 🤔 Plotting languages are arguably domain-specific, markup languages are arguably domain specific – naming is hard. 🤷🏿‍♂️

Shell

(with-eval-after-load 'org
  (message "Load Shell into Org Babel")
  (org-babel-do-load-languages 'org-babel-load-languages
                               (append org-babel-load-languages
                                       '((shell . t)))))

Dockerfile

;; https://github.com/spotify/dockerfile-mode
(use-package dockerfile-mode
  :straight (dockerfile-mode :type git
                             :host github
                             :repo "spotify/dockerfile-mode"))

Octave

Octave mode provides support for the Octave scientific programming language which is a popular FLOSS alternative to Matlab. From a glance at the Emacs git history it seems that this feature has been bundled in emacs for a while now, so we will simply assume it’s here and add the language to the org-babel-load-languages list to enable Org Babel exports.

(with-eval-after-load 'org
  (message "Load Octave into Org Babel")
  (org-babel-do-load-languages 'org-babel-load-languages
                               (append org-babel-load-languages
                                       '((octave . t)))))

Nix

;; https://github.com/NixOS/nix-mode
(use-package nix-mode
  :straight (nix-mode :type git
                      :host github
                      :repo "NixOS/nix-mode")

  :custom
  (nix-nixfmt-bin "nixpkgs-fmt"))

Org-babel support for nix shells

It is possible to define shell source blocks that can be evaluated through ob-shell but these environments don’t quite seem to be Nix aware. I’ve explored using the envrc package but these only seem to configure a) local buffers exec paths and environment variables and b) command invocations through shell-command-to-string neither of which cover configuration of ob-shell invocations.

Generate shebang lines for nix shells

In order to use nix shells in literate programs, we need an ability to eval shell code blocks in a Nix-aware manner. The standard shell executor, copies the contents of a code block into the tmp directory and executes it there. Since nix-shells are location dependent (because the shell.nix or any other .nix file in the source directory may be required to adequately run them), we provide a means to define a shebang line for nix-shell runs.

nix-shell ob-shell shebang generator

Let’s define a function to allow us to dynamically generate a valid shebang for shell blocks that will spawn a nix-shell to run the code in.

(defun vidbina/ob-shell-nix-shebang (&optional shell-file)
  "Shebang line for a nix-shell environment based on the buffer directory"
  (let ((shell-file (or (when (stringp shell-file) shell-file) "shell.nix"))
        (nix-file (expand-file-name shell-file
                                    (file-name-directory (buffer-file-name)))))
    (format "#!/usr/bin/env nix-shell\n#!nix-shell -i bash %s" nix-file)))

Testing against ../shell.nix which should roughly contain a superset of:

# save this as shell.nix
{ pkgs ? import <nixpkgs> {}}:

pkgs.mkShell {
  nativeBuildInputs = [ pkgs.hello ];
}

we can define a shell block with the shebang helper in the following manner to run the code inside of a nix-shell:

#+begin_src bash :shebang (vidbina/ob-shell-nix-shebang) :results verbatim
hello
#+end_src

which can be executed as follows with the result listed thereafter:

hello

The following example should fail because the shebang helper checks that the supplied argument is a valid string that can be expanded into a path:

#+begin_src bash :shebang (vidbina/ob-shell-nix-shebang (list "a")) :results none
hello
#+end_src
References
Define org-babel-execute for nix-develop
Background

Refer to <a href=”https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-shell.html#org9ad9ef2 “>Shell Code Blocks in Babel for details on how Org-babel executes shell blocks.

The org-babel-shell-initialize function defines specialized ob-execute handlers for every one of the supported shells in org-babel-shell-names (of which bash, sh and zsh are members).
(defun org-babel-shell-initialize ()
  "Define execution functions associated to shell names.
This function has to be called whenever `org-babel-shell-names'
is modified outside the Customize interface."
  (interactive)
  (dolist (name org-babel-shell-names)
    (eval `(defun ,(intern (concat "org-babel-execute:" name))
	       (body params)
	     ,(format "Execute a block of %s commands with Babel." name)
	     (let ((shell-file-name ,name))
	       (org-babel-execute:shell body params))))
    (eval `(defalias ',(intern (concat "org-babel-variable-assignments:" name))
	     'org-babel-variable-assignments:shell
	     ,(format "Return list of %s statements assigning to the block's \
variables."
		      name)))
    (eval `(defvar ,(intern (concat "org-babel-default-header-args:" name)) '()))))

The generalized ob-exec (short form for Org-babel execute 🤦🏿‍♂️) handler org-babel-execute:shell is the entrypoint for all shell execution tasks and is called by org-babel-execute-src-block which packages like ob-async retrofit (through the advice facility) to provide async execution capability.

(defun org-babel-execute:shell (body params)
  "Execute a block of Shell commands with Babel.
This function is called by `org-babel-execute-src-block'."
  (let* ((session (org-babel-sh-initiate-session
		   (cdr (assq :session params))))
	 (stdin (let ((stdin (cdr (assq :stdin params))))
                  (when stdin (org-babel-sh-var-to-string
                               (org-babel-ref-resolve stdin)))))
	 (results-params (cdr (assq :result-params params)))
	 (value-is-exit-status
	  (or (and
	       (equal '("replace") results-params)
	       (not org-babel-shell-results-defaults-to-output))
	      (member "value" results-params)))
	 (cmdline (cdr (assq :cmdline params)))
         (full-body (concat
		     (org-babel-expand-body:generic
		      body params (org-babel-variable-assignments:shell params))
		     (when value-is-exit-status "\necho $?"))))
    (org-babel-reassemble-table
     (org-babel-sh-evaluate session full-body params stdin cmdline)
     (org-babel-pick-name
      (cdr (assq :colname-names params)) (cdr (assq :colnames params)))
     (org-babel-pick-name
      (cdr (assq :rowname-names params)) (cdr (assq :rownames params))))))

The results of the org-babel-execute:shell call is a table as indicated by the org-babel-reassemble-table.

Note that the org-babel-sh-evaluate function is the main worker we need to pay attention to. It roughly handles 4 cases:

  1. when stdin or cmdline are defined → external shell script w/ stdin
  2. when session is defined → session evaluation
  3. if shebang is not empty, external shell script w/ or w/o shebang
  4. otherwise, (org-babel-eval EXEC_FILE BODY)
(let ((session-name "*dummy-session*")
      (body "echo hi")
      (params '())
      (stdin nil)
      (cmdline nil))
  (org-babel-sh-initiate-session session-name)
  (org-babel-sh-evaluate session-name body params stdin cmdline))
(org-babel-eval "xargs echo" "hi")

It takes a session that is initiated with “none” by default resulting to:

(org-babel-sh-initiate-session)

Initiating a session with a name yields a namesake buffer:

(org-babel-sh-initiate-session "my-temporary-session")

Note that params can be set through code-block headers

echo "hi"
Define nix-develop ob-execute handler

The nix-develop prompt will either default to “> ” or the value of the nixConfig.bash-prompt attribute.

The nixConfig.bash-prompt{,-{prefix,suffix}} can be defined to specify the PS1 variable within the nix develop shell and is defined in the <a href=”https://github.com/NixOS/nix/blob/bf89cd95a4af35ab15f7fad3186c8f6190f87c84/src/nix/develop.cc “>nix/src/nix/develop.cc.Prompt

We describe our prompt matcher through the following regexp:

"^>\s+"

We verify the previously defined regexp by calling the re-search-forward

(re-search-forward <<nix-develop-prompt-regexp>> nil t)

Execution of the previously listed re-search-forward call should move the cursor to the “> here” line in the following block:

>no … $ not a valid prompt > here > here too

Define the previously defined regexp as the default nix-develop prompt:

(defcustom nix-develop-default-prompt-regexp <<nix-develop-prompt-regexp>>
  "Custom prompt for nix-develop"
  :type 'string
  :group 'nix-develop)
Execute Handler

In order to process code blocks through Org-Babel execute, we define a org-babel-execute handler.

(defun org-babel-execute:nix-develop (body params)
  "Execute a block of nix develop commands with Babel."
  (save-window-excursion
    (let* ((shell-buffer (org-babel-sh-initiate-session "*nix-develop*"))
           (prompt-regexp nix-develop-default-prompt-regexp))
      (org-babel-comint-with-output
          (shell-buffer org-babel-sh-eoe-output t body)
        (dolist (line (append (list "nix develop")
                              (split-string (org-trim body) "\n")
                              (list org-babel-sh-eoe-indicator)))
          (insert line)
          (comint-send-input nil t)
          (while (save-excursion
                   (goto-char comint-last-input-end)
                   (not (re-search-forward
                         prompt-regexp nil t)))
            (accept-process-output
             (get-buffer-process (current-buffer)))))))))
Define syntax major mode for highlighting

We define sparse keymap:

(defvar nix-develop-mode-map
  (let ((map (make-sparse-keymap)))
    map))

Define a syntax table that is inherits from shell-mode since we’re expecting code blocks to only contain shell-like syntax:

(defvar nix-develop-mode-syntax-table
  (make-syntax-table shell-mode-syntax-table))

Derive a major mode from comint-mode because we want to do the interactive thing:

(define-derived-mode nix-develop-mode comint-mode "Nix Develop"
  "Major mode for `nix-develop'"
  (setq comint-prompt-regexp nix-develop-default-prompt-regexp))
Example: Code blocks of different major-modes
echo "hi"
for i in a b; do testing; done
echo "hi"
for i in a b; do testing; done
echo "hi"
for i in a b; do testing; done
Compose nix-develop-mode and ob-execute handler
<<nix-develop-mode>>
<<nix-develop-ob-execute>>

(provide 'nix-develop-mode)
Prep Reddit question
I've noticed that calling `define-derived-mode` multiple times and then refreshing syntax highlighing on a code block (by reentering the syntax descriptor) to another buffer to test that mode in a code block doesn't seem to reflect the changes made in the `define-derived-mode` call.

As an example let's define a dummy mode blah and use it in a code block as follows:

```
#+begin_src blah :results verbatim
echo "hi"
#+end_src
```

A. Deriving a mode from shell-mode

```
(define-derived-mode blah-mode
  sh-mode "Nix Develop"
  "Major mode for `nix-develop`")
```

B. Deriving a mode from sh-mode

```
(define-derived-mode blah-mode
  emacs-lisp-mode "Nix Develop"
  "Major mode for `nix-develop`")
```

Running A and then opening/reloading the buffer (through `revert-buffer-quick`) and then running B and reloading the buffer again doesn't seem to render different results in the buffer.

When we reload Emacs and run B, the rendering in the source block seems quite different.

```
#+begin_src blah :results verbatim
echo "hi"
#+end_src
```
I am aware that I can define a nix-shell shebang as follows

```shell
#!/usr/bin/env nix-shell
#!nix-shell -i bash /PATH/TO/shell.nix
```

but I am trying to figure out how to spawn a nix shell interpreter in a Flake-based configuration where I may have to use nix-develop

Web

;; https://github.com/fxbois/web-mode
(use-package web-mode
  :straight (web-mode :type git
                      :host github
                      :repo "fxbois/web-mode"))

Vue

;; https://github.com/fxbois/web-mode
(use-package web-mode
  :straight (web-mode :type git
                      :host github
                      :repo "fxbois/web-mode"))

General Purpose Programming Languages

Python

Use pyright-langserver as default LSP for Python

(rassq-delete-all 'python-mode eglot-server-programs)
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(python-mode . ("pyright-langserver" "--stdio"))))
Helper to stub configuration file for pyright

The configuration file for pyright may look as follows:

{
    "venvPath": "/absolute/path/to/dir/",
    "venv": ".venv",
    "verboseOutput": false,
    "typeCheckingMode": "strict",
    "useLibraryCodeForTypes": true,
}

Most important is setting the venvPath and venv directories since we may have varying setups between projects and just want the language server to pick up the right pieces. The following helper just naively stubs the config file for the current working directory with the .venv subdirectory as the virtualenv path.

Inspired from https://robbmann.io/posts/emacs-eglot-pyrightconfig/

(defun vidbina/init-eglot-pyright-config ()
  "Stub a pyrightconfig.json file for a project"
  (interactive)

  (let* ((project-dir default-directory)
         (venv-dir ".venv"))

    (message (format "🐍 Writing config for %s with venv in %s" project-dir venv-dir))
    (with-temp-buffer
      (insert (json-serialize `((venvPath . ,project-dir)
                                (venv . ,venv-dir)
                                (verboseOutput . :false)
                                (typeCheckingMode . "strict")
                                (useLibraryCodeForTypes . t)
                                (defineConstant . ((DEBUG . t))))))
      (json-pretty-print-buffer)
      (append-to-file (point-min) (point-max) (expand-file-name "pyrightconfig.json" project-dir)))))

Go (Golang)

;; https://github.com/dominikh/go-mode.el
(use-package go-mode
  :straight (go-mode :type git
                     :host github
                     :repo "dominikh/go-mode.el"))

JavaScript (ECMAScript or ES)

Since JavaScript is everywhere, let’s make sure we can at least read it with ease.

;; https://github.com/redguardtoo/js-comint
(use-package js-comint
  :straight (js-comint :type git
                       :host github
                       :repo "redguardtoo/js-comint")
  :hook (inferior-js-mode . (lambda ()
                              (add-hook 'comint-output-filter-functions 'js-comint-process-output)))
  :config
  (define-key js-mode-map [remap eval-last-sexp] #'js-comint-send-last-sexp)
  (define-key js-mode-map (kbd "C-c b") 'js-send-buffer)
  :custom
  (js-indent-level 2))

TypeScript

Superset of JavaScript that folks really should be using instead of just vanilla JS but we’re not here to judge. 🤷🏿‍♂️

TSI

https://vxlabs.com/2022/06/12/typescript-development-with-emacs-tree-sitter-and-lsp-in-2022/#ensure-for-tsx-configure-for-tree-sitter-based-indentation

;; https://github.com/orzechowskid/tsi.el/
;; great tree-sitter-based indentation for typescript/tsx, css, json
(use-package tsi
  :after tree-sitter
  :straight (tsi :type git
                 :host github
                 :repo "orzechowskid/tsi.el")
  ;; define autoload definitions which when actually invoked will cause package to be loaded
  :commands (tsi-typescript-mode tsi-json-mode tsi-css-mode)
  :init
  (add-hook 'typescript-mode-hook (lambda () (tsi-typescript-mode 1)))
  (add-hook 'json-mode-hook (lambda () (tsi-json-mode 1)))
  (add-hook 'css-mode-hook (lambda () (tsi-css-mode 1)))
  (add-hook 'scss-mode-hook (lambda () (tsi-scss-mode 1))))

Typescript-mode

  • State “PROTOTYPE” from “CANCELED” [2023-07-04 Tue 20:44]
    Reopening typescript-mode as there seems to be a bit of a disconnect between major modes and the tree-sitter major modes that I really need to understand.
  • State “CANCELED” from [2023-07-04 Tue 14:15]
    Just got eglot in tsx-ts-mode working again so cordially fuck this.

Development of this package has been halted, so we’re stopping our use of this.

;; https://github.com/emacs-typescript/typescript.el
(use-package typescript-mode
  :straight
  (typescript-mode :type git
                   :host github
                   :repo "emacs-typescript/typescript.el")
  :after flyspell tree-sitter
  :delight
  (typescript-mode "ts")
  :custom
  (typescript-indent-level 2)

  ;; https://vxlabs.com/2022/06/12/typescript-development-with-emacs-tree-sitter-and-lsp-in-2022/#ensure-for-tsx-configure-for-tree-sitter-based-indentation
  :config
  ;; we choose this instead of tsx-mode so that eglot can automatically figure out language for server
  ;; see https://github.com/joaotavora/eglot/issues/624 and https://github.com/joaotavora/eglot#handling-quirky-servers
  (define-derived-mode typescriptreact-mode typescript-mode
    "TypeScript TSX")

  ;; use our derived mode for tsx files
  (add-to-list 'auto-mode-alist '("\\.tsx?\\'" . typescriptreact-mode))
  ;; by default, typescript-mode is mapped to the treesitter typescript parser
  ;; use our derived mode to map both .tsx AND .ts -> typescriptreact-mode -> treesitter tsx
  (add-to-list 'tree-sitter-major-mode-language-alist '(typescriptreact-mode . tsx)))

Lua

In order to edit Lua code for our neovim configs, Hammerspoon and just good measure because Lua isn’t that uncommon of a language to bump into, we install the lua-mode package.

;; https://github.com/immerrr/lua-mode
(use-package lua-mode
  :straight (lua-mode :type git
                      :host github
                      :repo "immerrr/lua-mode"))

Vim

;; https://github.com/mcandre/vimrc-mode
(use-package vimrc-mode
  :straight (vimrc-mode :type git
                      :host github
                      :repo "mcandre/vimrc-mode"))

Clojure

;; https://github.com/clojure-emacs/clojure-mode
(use-package clojure-mode
  :straight (clojure-mode :type git
                          :host github
                          :repo "clojure-emacs/clojure-mode")
  :config
  <<clojure-config>>)

The ob-clojure package provides Org-Babel support for Clojure code blocks.

(require 'ob-clojure)

Refer to the Org-babel-clojure documentation for instructions on how to install your environment to use Clojure with Emacs.

CIDER

;; https://github.com/clojure-emacs/cider
(use-package cider
  :straight (cider :type git
                   :host github
                   :repo "clojure-emacs/cider")
  :config
  (setq org-babel-clojure-backend 'cider
        cider-lein-parameters "with-profile -user repl :headless :host localhost"))

Kotlin

;; https://github.com/Emacs-Kotlin-Mode-Maintainers/kotlin-mode
(use-package kotlin-mode
  :straight (kotlin-mode :type git
                         :host github
                         :repo "Emacs-Kotlin-Mode-Maintainers/kotlin-mode"))

Swift

;; https://github.com/swift-emacs/swift-mode
(use-package swift-mode
  :straight (swift-mode :type git
                        :host github
                        :repo "swift-emacs/swift-mode"))

Haskell

In order to conveniently read and write Haskell, I rely on haskell-mode. Note that haskell-tng is a fork from haskell-mode that may be worth looking into in your case. read the manual for more information.

In order to configure interactive mode, we follow the setup instructions from the manual.

;; https://github.com/haskell/haskell-mode
(use-package haskell-mode
  :straight (haskell-mode :type git
                          :host github
                          :repo "haskell/haskell-mode")
  :config
  (require 'haskell-interactive-mode)
  (require 'haskell-process)
  :hook ((haskell-mode . haskell-unicode-input-method-enable)
         (haskell-mode . interactive-haskell-mode))
  :custom
  (haskell-process-suggest-remove-import-lines t)
  (haskell-process-auto-import-loaded-modules t)
  (haskell-process-log t)
  (haskell-stylish-on-save t))

Please note that GHCi, HLint and stylish-haskell are needed for this configuration to work.

Haskell Environment Configuration in Nix

For convenience, I use direnv to manage my environments. Considering that I am a Nix user and the configuration described above needs GHCi, Hlint and stylish-haskell installed, I just have to see to it that a project directory tree contains an .envrc file that contains the use nix string to relegate env configuration to the nix configuration and then populate default.nix to contain description of the needed environment.

So, .envrc should contain the following:

use nix

and my default.nix file will contain something to the tune of the snippet below.

{ sources ? import ./nix/sources.nix }:

let
  nixpkgs = import sources.nixpkgs {};
in
nixpkgs.mkShell {
  buildInputs = with nixpkgs.haskellPackages; [
    ghci
    hlint
    stylish-haskell
  ];
}

To add another layer of convenience or complexity, depending on how you want to look at it 🤷🏿‍♂️, I manage my nix packages with niv in order to decouple the project packages from my system configuration (i.e.: every project installs packages in reference to a pinned package repository that remains the same even if the system repository changes over time which improves reproducability). This is where the ./nix/sources.nix bit comes into the picture – that’s a niv thing. In order to populate the nix/sources.nix and nix/sources.json files that nix needs, I have to run niv init inside of the directory where the default.nix resides. After all of this is done, we have to allow direnv to evaluate the files within the directory to autoload our environment. I sometimes do this within emacs with (envrc-allow) but you can also do this from a terminal with the command direnv allow.

Elm

;; https://github.com/jcollard/elm-mode
(use-package elm-mode
  :straight (elm-mode :type git
                      :host github
                      :repo "jcollard/elm-mode"))

Rust

;; https://github.com/rust-lang/rust-mode
(use-package rust-mode
  :straight (rust-mode :type git
                       :host github
                       :repo "rust-lang/rust-mode"))

AppleScript

At times, I may need to edit AppleScript code for some machine-side automations which I may trigger with Hammerspoon.

;; https://github.com/emacsorphanage/applescript-mode
(use-package applescript-mode
  :straight (applescript-mode :type git
                              :host github
                              :repo "emacsorphanage/applescript-mode"))

Misc

Misc bells and whistles from formatting aids to LLM-enabled magic.

EditorConfig

;; https://github.com/editorconfig/editorconfig-emacs#readme
(use-package editorconfig
  :straight (editorconfig :type git
                          :host github
                          :repo "editorconfig/editorconfig-emacs")
  :config
  (editorconfig-mode 1)
  :delight
  (editorconfig-mode "🎛️"))

Debug editorconfig conflict

See editorconfig/editorconfig-emacs#264 regarding indent_size being ignored editorconfig is being ignored.

Rainbow Delimiters

;; https://github.com/Fanael/rainbow-delimiters
(use-package rainbow-delimiters
  :straight (rainbow-delimiters :type git
                                :host github
                                :repo "Fanael/rainbow-delimiters")
  :hook ((emacs-lisp-mode . rainbow-delimiters-mode)
         (prog-mode . rainbow-delimiters-mode))

  ;; ;; https://github.com/patrickt/emacs
  ;; ((prog-mode) . rainbow-delimiters-mode)
  )

Tree-Sitter

Read https://www.masteringemacs.org/article/how-to-get-started-tree-sitter for a good intro into using Tree-Sitter.

Also see TypeScript development with Emacs, tree-sitter and LSP in 2022 article by vxlabs and Nathan’s write-up on building tree-sitter-langs which I all cherry-picked insights from.

See if treesitter is available in a buffer by running:

(treesit-available-p)

Install Elisp bindings

Use built-in tree-sitter
  • ⚠️ [2023-07-04 Tue 20:40] I initally thought of test the built-in/integrated tree-sitter in Emacs 29+ as per the recommendation on the elisp-tree-sitter README but am realising that my current build of Emacs may not have tree-sitter bundled into it. 😅 Don’t use bleeding edge Emacs but just use the stable-ish version 29 if you don’t want to screw around.

Fiddled around with this for a bit and stumbled into a variety of errors, so I triehich led to the the explicit definition of variable treesit-language-source-alist which treesit-auto needs to get the job done. While playing around, I noticed that I misunderstood how tree-sitter-langs and treesit-auto factor into the picture. My current understanding as per [2023-07-04 Tue 15:19] is that:

  • tree-sitter-langs provides lang bundles that need to be installed
(use-package treesit
  :straight (:type built-in)
  ;;:autoload treesit-install-language-grammar
  :commands (treesit-install-language-grammar nf/treesit-install-all-languages)
  :init
  (setq treesit-language-source-alist
        <<treesit-languages>>
        )
  :config
  <<treesit-config>>
  (defun nf/treesit-install-all-languages ()
    "Install all languages specified by `treesit-language-source-alist'."
    (interactive)
    (let ((languages (mapcar 'car treesit-language-source-alist)))
      (dolist (lang languages)
	(treesit-install-language-grammar lang)
	(message "`%s' parser was installed." lang)
	(sit-for 0.75)))))
Reinstall Emacs with built-in tree-sitter

Needed to downgrade to Emacs 29 (from 30) in order to get the built-in tree-sitter bindings. Weirdly the tree-sitter.el file in Emacs master lacks things like treesit-install-language-grammar which is needed to properly use tree-sitter because tree-sitter without grammars isn’t very useful. 🤷🏿‍♂️

tree-sitter major mode mappings

(setq major-mode-remap-alist
      '((yaml-mode . yaml-ts-mode)
        (bash-mode . bash-ts-mode)
        (js2-mode . js-ts-mode)
        (typescript-mode . typescript-ts-mode)
        (json-mode . json-ts-mode)
        (css-mode . css-ts-mode)
        (python-mode . python-ts-mode)))

tree-sitter extension mappings

(add-to-list 'auto-mode-alist '("\\.[tj]sx?\\'" . tsx-ts-mode))

tree-sitter Grammars

See a listing of available grammars at https://tree-sitter.github.io/tree-sitter/ with links to their respective repositories.

alist of language mappings

Just because treesit-language-source-alist needs this (for the treesit config), we define the language to grammar repo mapping here for “reuse”.

'((bash . ("https://github.com/tree-sitter/tree-sitter-bash"))
  (c . ("https://github.com/tree-sitter/tree-sitter-c"))
  (cpp . ("https://github.com/tree-sitter/tree-sitter-cpp"))
  (css . ("https://github.com/tree-sitter/tree-sitter-css"))
  (go . ("https://github.com/tree-sitter/tree-sitter-go"))
  (html . ("https://github.com/tree-sitter/tree-sitter-html"))
  (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript"))
  (json . ("https://github.com/tree-sitter/tree-sitter-json"))
  (lua . ("https://github.com/Azganoth/tree-sitter-lua"))
  (make . ("https://github.com/alemuller/tree-sitter-make"))
  (org . ("https://github.com/milisims/tree-sitter-org"))
  (ocaml . ("https://github.com/tree-sitter/tree-sitter-ocaml" "ocaml/src" "ocaml"))
  (python . ("https://github.com/tree-sitter/tree-sitter-python"))
  (php . ("https://github.com/tree-sitter/tree-sitter-php"))
  (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "typescript/src" "typescript"))
  (ruby . ("https://github.com/tree-sitter/tree-sitter-ruby"))
  (rust . ("https://github.com/tree-sitter/tree-sitter-rust"))
  (sql . ("https://github.com/m-novikov/tree-sitter-sql"))
  (toml . ("https://github.com/tree-sitter/tree-sitter-toml"))
  (zig . ("https://github.com/GrayJack/tree-sitter-zig")))
tree-sitter-langs: Bundle of tree-sitter grammars

Bundle of tree-sitter grammars. You may need to run tree-sitter-langs-install-grammars for this to work.

;; https://github.com/emacs-tree-sitter/tree-sitter-langs
(use-package tree-sitter-langs
  :straight (tree-sitter-langs :type git
                               :host github
                               :repo "emacs-tree-sitter/tree-sitter-langs")
  :after tree-sitter)
treesit-auto: Automatically install tree-sitter grammars

Automatically installs treesitter grammars and calls the function treesit-install-language-grammar to get the job done.

(require 'treesit)
;; https://github.com/renzmann/treesit-auto.git
(use-package treesit-auto
  :straight (treesit-auto :type git
                          :host github
                          :repo "renzmann/treesit-auto")
  :config
  (global-treesit-auto-mode))

Failing with void-variable treesit-language-source-alist

Structural Editing

Structural editing allows us to treat structural units of code with specific actions like movement, splicing and selection.

Combobulate

The combobulate package is authored by the same Mickey who brought us Mastering Emacs.

;; https://github.com/mickeynp/combobulate
(use-package combobulate
  :straight (combobulate :type git
                         :host github
                         :repo "mickeynp/combobulate"))

Formatting

Apheleia

Apheleia helps to auto-format code upon saving. With this in place, we don’t need to bother too much about editor-level indentation since we know it’ll get fixed once we save anyways.

;; https://github.com/radian-software/apheleia
(use-package apheleia
  :straight (apheleia :type git
                         :host github
                         :repo "radian-software/apheleia")
  :ensure t
  :config
  (apheleia-global-mode +1)
  :delight
  (apheleia-mode "👨🏿‍🏭"))

<<highlight-indent-guides>> Highlight Indent Guides

;; https://github.com/DarthFennec/highlight-indent-guides
(use-package highlight-indent-guides
  :straight (highlight-indent-guides :type git
                                     :host github
                                     :repo "DarthFennec/highlight-indent-guides")
  :custom
  (highlight-indent-guides-method 'column))

<<paredit>> Paredit

In order to simplify editing LISPs, paredit can be used to assist in keeping forms balanced (i.e.: ensuring that a form always has as many opening as closing parenthesis).

;; https://github.com/emacsmirror/paredit
(use-package paredit
  :straight (paredit :type git
                     :host github
                     :repo "emacsmirror/paredit")
  <<paredit-config>>)

One of the killer features of paredit is command transpose-sexp (keybinding: C-M-t) which swaps left-hand and right-hand sexps. In the example of the cursor being represented as _, we can transpose from (a_ b c) to (b_ a c). Especially when one has multi-line complex sexps where starting and ending parens may be complicating the ability to just swap sexps by moving lines, this comes in handy.

(do-something-useful 'dry-run
                     ;; Note how neither of the following lines can be swapped
                     ;; without breaking elisp syntax.
                     '((get-value a)
                       (get-value b)
                       (get-value c)))

Set lighter

:delight
(paredit-mode "🛝")

Set global binding

Enable paredit mode using the C-c v ( binding.

:bind (("C-c v (" . paredit-mode))

Usage

Once paredit mode is enabled, we can do the following:

  • C-right slurp
    • src_elisp[:exports code]{(a (here) b)} → src_elisp[:exports code]{(a (here b))}
  • C-left barf
    • src_elisp[:exports code]{(a (here b))} → src_elisp[:exports code]{(a (here) b)}
  • M-S split sexp
    • with point before b, src_elisp[:exports code]{(a b)} → src_elisp[:exports code]{(a) (b)}
  • C-M-b / C-M-f move backward/forward
  • C-M-u / C-M-n move backward/forward out of enclosing list

You can enter a special edit mode by placing point on the codeblock below and entering C-c '.

'(a (here) b)

(progn
  (message "hi")
  (+ 40 2) :42)

(progn
  (let ((a 4))
    '(a (b c) d)
    (+ (* 12 3)
       (1+ 12))))


(+ 1 (* 2 3) 4)

<<inheritenv>> inheritenv

The inheritenv package configures background processes to adopt the process environment and exec-path of the calling Emacs buffer. This package is used by envrc which means that we don’t really have to do anything if we configure the envrc to run the show for us.

;; https://github.com/purcell/inheritenv
(use-package inheritenv
  :straight (inheritenv :type git
                        :host github
                        :repo "purcell/inheritenv"))

<<envrc>> envrc

By using the envrc package, buffer-local variables can be managed through the configuration of the direnv .envrc file.

;; https://github.com/purcell/envrc
(use-package envrc
  :straight (envrc :type git
                   :host github
                   :repo "purcell/envrc")
  :after inheritenv
  :delight
  (envrc-mode "📦")
  :hook (after-init . envrc-global-mode)
  :bind-keymap ("C-c e" . envrc-command-map))

We bind C-c e to envrc to simplify access to mode toggles and reloading facilities.

The use of this package, will arm shell-command-to-string to call inheritenv through the envrc-propagate-environment call.

Configure exec path

When using changes to the path may go unnoticed to Emacs which results to shell blocks in Org files not having the proper path configurations and therefore not being able to find the right executables.

;; https://github.com/purcell/exec-path-from-shell
(use-package exec-path-from-shell
  :straight (exec-path-from-shell :type git
                                  :host github
                                  :repo "purcell/exec-path-from-shell")
  :config (when (daemonp)
            (exec-path-from-shell-initialize)))

https://emacs.stackexchange.com/questions/53773/best-way-to-make-org-babel-blocks-aware-of-my-path-and-other-environment-variab

AI

AIDE

In order to use OpenAI’s GPT models directly inside of Emacs, we are using the AIDE package.

;; https://github.com/junjizhi/aide.el
(use-package aide
  :straight (aide :type git
                  :host github
                  :repo "junjizhi/aide.el")
  :custom
  (aide-completions-model "text-davinci-003")
  (aide-max-output-tokens 1000)
  (aide-openai-api-key-getter (lambda ()
                                (auth-source-pass-get 'secret "openai.com/[email protected]/api-key-2023.04.18-emacs-vidbina"))))

A newer version of the aide-openai-complete that uses a more general path but specified the model in the JSON payload is:

(defun vidbina/aide-openai-chat-complete (instruction target)
  "Return the prompt answer from OpenAI API.
API-KEY is the OpenAI API key.

PROMPT is the prompt string we send to the API."
  (message "Let's get schwifty")
  (let* ((result nil)
         (auth-value (format "Bearer %s" (funcall aide-openai-api-key-getter)))
         (model "gpt-3.5-turbo")
         (data `(("model" . "gpt-3.5-turbo")

                 ("top_p" . ,aide-top-p)
                 ("max_tokens" . ,aide-max-tokens)
                 ("messages" . [
                                (("role" . "system") ("content" . ,instruction))
                                (("role" . "user") ("content" . ,target))
                                ])))
         ;;(json-data (json-encode data))
         (endpoint "https://api.openai.com/v1/chat/completions"))
    (message "Payload %s" data)
    (message "Target %s" endpoint)
    (vidbina/kill (json-encode data))
    (request endpoint
      :type "POST"
      :data (json-encode data)
      :headers `(("Authorization" . ,auth-value) ("Content-Type" . "application/json"))
      :sync t
      :parser 'json-read
      :success (cl-function
                (lambda (&key data &allow-other-keys)

                  (message "Yes! %s" data)
                  (let ((top-choice-message (alist-get 'message (elt (alist-get 'choices data) 0))))
                    (setq result (alist-get 'content top-choice-message))
                    (message "%s: %s" (alist-get 'role top-choice-message) (alist-get 'content top-choice-message)))))
      :error (cl-function (lambda (x) (message "Error: %s" x))))
    result))

Here is a pair programming helper that uses aide under the hood:

(defun vidbina/gpt-pair-prog (start end)
  (interactive "r")
  (if (region-active-p)
      (progn (message "Region is selected!")
             (let* ((instruction (read-string ">" "As a senior programmer, "))
                    (region (buffer-substring-no-properties start end))
                    ;;(result (aide--openai-complete-string region))
                    (result-new (vidbina/aide-openai-chat-complete instruction region)))
               (vidbina/kill result-new)))
    (message "No region is selected.")))
As an experienced ROLE, PROMPT or return the original code with a comment stating: AI am unsure.

CODE
;; TODO: Fix
(defun vidbina/morph-code-with-llm (start end role question)
  "What is this"
  (interactive "rsWhat is your buddies role: \nsWhat do you want to know?")

  (let* ((region (buffer-substring-no-properties start end))
         (prompt (let ((template (string-trim "
<<vidbina-prompt-template>>
"))
                       (prompt "do something funny")
                       (buddy-role "senior TypeScript"))
                   (->> template
                        (string-replace "TYPE" buddy-role)
                        (string-replace "PROMPT" "hi there")
                        (string-replace "CODE" "..."))
                   ))
         (result (aide-openai-edits (funcall aide-openai-api-key-getter) prompt region)))
    (goto-char end)
    (if result
        (progn
          (kill-region start end)
          (insert "\n" result)
          (fill-paragraph)
          (let ((x (make-overlay end (point))))
            (overlay-put x 'face '(:foreground "orange red")))
          result)
      (message "Empty result"))))
(defun vidbina/morph-code-with-llm (start end role question)
  (interactive "r\nsWhat is your buddies role: \nsWhat do you want to know?")

  (let* ((region (buffer-substring-no-properties start end))
         (prompt (let ((template (string-trim "
<<vidbina-prompt-template>>
"))
                       (prompt "do something funny")
                       (buddy-role "senior TypeScript"))
                   (->> template
                        (string-replace "TYPE" buddy-role)
                        (string-replace "PROMPT" "hi there")
                        (string-replace "CODE" "..."))))
         (result (aide-openai-edits (funcall aide-openai-api-key-getter) "Rephrase the text" region)))
    (goto-char end)
    (if result
        (progn
          (kill-region start end)
          (insert "\n" result)
          (fill-paragraph)
          (let ((x (make-overlay end (point))))
            (overlay-put x 'face '(:foreground "orange red")))
          result)
      (message "Empty result"))))
(aide-openai-complete (funcall aide-openai-api-key-getter) "Define an agenda for a meeting")
Add chatgpt helper to aide

GPTel

;; https://github.com/karthink/gptel
(use-package gptel
  :straight (gptel :type git
                   :host github
                   :repo "karthink/gptel")
  :config
  (setq gptel-default-mode 'org-mode)
  (setq gptel-api-key (lambda ()
                        (auth-source-pass-get 'secret "openai.com/[email protected]/api-key-2023.04.18-emacs-vidbina")))

  :custom
  <<gptel-custom>>
  )
Custom directives
(gptel-directives
 '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
   (programming . "You are a large language model and a careful programmer. Provide code and only code as output without any additional text, prompt or note.")
   (writing . "You are a large language model and a writing assistant. Respond concisely.")
   (chat . "You are a large language model and a conversation partner. Respond concisely.")
   (vid . "You are a technical analyst with a strong background in EE an CS. Respond concisely and assume that the reader has the same background which warrants the avoidance of explanation of technical concepts unless explicitly asked for.")
   ))

Copilot

https://www.irfanhabib.com/2022-04-26-setting-up-github-copilot-in-emacs/

(use-package copilot
  :straight (:host github :repo "zerolfx/copilot.el"
                   :files ("dist" "copilot.el"))
  :ensure t
  :config
  (define-key copilot-completion-map (kbd "<tab>") 'copilot-accept-completion)
  (define-key copilot-completion-map (kbd "TAB") 'copilot-accept-completion)
  (define-key copilot-completion-map (kbd "M-n") 'copilot-next-completion)
  (define-key copilot-completion-map (kbd "M-p") 'copilot-previous-completion))

Flymake

(with-eval-after-load 'flymake
  ;; Set flymake bindings
  (define-key flymake-mode-map (kbd "M-n") 'flymake-goto-next-error)
  (define-key flymake-mode-map (kbd "M-p") 'flymake-goto-prev-error)
  <<flymake-helpers>>)

For the ease of use, we provide a helper to enter the line of interest when we enter the consult-flymake completion interface:

(defun vidbina/jump-to-active-line-in-consult-flymake ()
  "Jump to the current line in consult-flymake"
  (let* ((target-line (line-number-at-pos))
         (timer (run-at-time 1 nil
                             `(lambda ()
                                ;; Stubbing cancel hook
                                (defun vidbina/jump-to-active-line-in-consult-flymake--cancel ()
                                  (message "🪂 Cancelling timer")
                                  (advice-remove 'vertico-exit #'vidbina/jump-to-active-line-in-consult-flymake)
                                  (advice-remove 'exit-minibuffers #'vidbina/jump-to-active-line-in-consult-flymake))

                                (message "🪂 Arming timer cancellation on minibuffer escape")
                                ;; Arm (abort-minibuffers) and (exit-minibuffers) called by vertico-exit to cancel jump helper
                                (advice-add 'abort-minibuffers :before #'vidbina/jump-to-active-line-in-consult-flymake--cancel)
                                (advice-add 'exit-minibuffers :before #'vidbina/jump-to-active-line-in-consult-flymake--cancel)
                                (message "🪂 Executing jump to %s in buffer %s" ,(number-to-string target-line) (buffer-name))
                                ;; Note that entering the digits is not enough to update the position in vertico
                                (mapcar (lambda (x) (self-insert-command 1 x)) ,(number-to-string target-line))
                                (insert "")))))
    (message "🪂 Armed jumper to %s" (number-to-string target-line))))

(advice-add 'consult-flymake :before #'vidbina/jump-to-active-line-in-consult-flymake)

LSP

Eglot

Eglot (Emacs Polyglot) is the integrated Emacs LSP integration.

;; https://github.com/joaotavora/eglot
(use-package eglot
  :straight (eglot :type git
                   :host github
                   :repo "joaotavora/eglot")
  ;; :hook
  ;; (eglot-managed-mode-hook . (lambda ()
  ;;                              ;; Show flymake diagnostics first.
  ;;                              (setq eldoc-documentation-functions
  ;;                                    (cons #'flymake-eldoc-function
  ;;                                          (remove #'flymake-eldoc-function eldoc-documentation-functions)))
  ;;                              ;; Show all eldoc feedback.
  ;;                              (setq eldoc-documentation-strategy #'eldoc-documentation-compose)))
  :custom
  (eglot-autoshutdown t)

  :bind (("C-c j" . eglot)
         :map eglot-mode-map
         ("C-c j f d" . eglot-find-declaration)
         ("C-c j f i" . eglot-find-implementation)
         ("C-c j f t" . eglot-find-typeDefinition)
         ("C-c j j j" . eglot)
         ("C-c j j r" . eglot-reconnect)
         ("C-c j h s" . eglot-signature-eldoc-function)
         ("C-c j h h" . eglot-hover-eldoc-function)
         ("C-c j \\" . eglot-format)
         ("C-c j k" . eglot-shutdown)
         ("C-c j j k" . eglot-rename)))

Set eglot-server-programs to configure which LSP servers you want to use for the different modes. Consult https://github.com/joaotavora/eglot#connecting-to-a-server for a list of LSPs that you can consider for different languages.

Off-topic: Org-mode LSP server

https://list.orgmode.org/[email protected]/t/

<<nix-config>> [0%] Nix Configuration

We use Nix (the language) to define our Emacs builds for GNU/Linux and Darwin (macOS) systems. For both systems, we need to build and install Emacs and then setup the emacs configuration directory.

<<nix-emacs-build>> Build Emacs

  • State “TODO” from [2023-10-04 Wed 21:28]
    Refactor in order to DRY this up since we’re having some conflicting information here and also some broken shit.

GNU/Linux

  • State “TODO” from [2023-10-04 Wed 22:15]
    Fix the GNU/Linux config since we haven’t used this for a minute. Just spin up a VM in UTM or VirtualBox to quickly test this out.
# Tangled from README.org
{ config, pkgs, lib, options, ... }:

let
  <<nix-let-bindings>>
in
{
  <<nix-home-emacsdir-source>>

  home.packages = with pkgs; [
    cask

    # General packages
    <<nix-packages>>

    # Linux packages
    <<nix-linux-packages>>
  ];

  <<nix-home-programs>>

  <<nix-linux-services>>

  nixpkgs.overlays = [
    <<nix-linux-overlays>>
  ];

  <<nix-linux-mime>>
}

Darwin (macOS)

# Tangled from README.org
{ pkgs, ... }:

{
  environment.systemPackages = with pkgs; [
    <<nix-packages>>
    <<nix-darwin-packages>>
  ];

  nixpkgs.overlays = [
    <<nix-darwin-overlays>>
  ];

  <<nix-darwin>>
}

Cleanup config by specializing nix-packages in listing above and fixing emacs/default.nix for Linux

Currently, I’m sloppily tangling package listing into the ref nix-packages which will break in Linux if I don’t have a Linux-specific my-emacs overlay defined. In principle, the commenting of the following section, should not affect a Linux-specific config and now we tangle into both file:emacs/default.nix and file:emacs/nix-darwin.nix.x

Install Emacs (launchd) service through nix-darwin and package through (home-manager)

In order to avoid having to figure out the mess with activation scripts, we can install the Emacs service through nix-darwin such that we have the long-running daemon available and simultaneously install the same Emacs as a home-manager package, such that we have the Spotlight-visible reference created for us (in the ~/Applications/Home Manager Apps directory). It is important to install the exact same version of Emacs in nix-darwin as well as home-manager to avoid version incompatibility issues.

services.emacs = {
  enable = true;
  package = pkgs.my-emacs;
};

Because Spotlight doesn’t see Emacs if it is only installed as a service, we also list it as a regularly-installed system package.

my-emacs

Auto-dark mode on macOS

In order to switch themes automatically when the macOS system theme changes, we can use auto-dark-emacs.

(when (eq system-type 'darwin)
  (message "🌕 Setup auto-dark on darwin")
  (use-package auto-dark
    :after modus-themes
    :straight (auto-dark :type git
                         :host github
                         :repo "LionyxML/auto-dark-emacs")
    :hook (auto-dark-dark-mode-hook . vidbina/theme-switch-to-dark)
    :hook (auto-dark-light-mode-hook . vidbina/theme-switch-to-light))
    :config (auto-dark-mode t))

Account of lacking --dired support in ls for MacOS

On MacOS the ls util does not support the --dired flag which may result in the following error:

insert-directory: Listing directory failed but ‘access-file’ worked

Address this by using ls from coreutils instead of the MacOS-native variant.

;; https://stackoverflow.com/a/42038174
(when (string= system-type "darwin")
  (setq insert-directory-program "/opt/homebrew/bin/gls")
  (setq dired-use-ls-dired t))

Package overlays

<<nix-overlays-linux>> GNU/Linux Overlays

Using emacs-overlay, we define an Emacs build that bundles packages that are difficult to install just with straight on account of non-elisp dependencies such as system dependencies that may require special privileges of a specialized build process.

# Imports before overlaying
<<nix-linux-overlay-imports>>

# Overlay custom Emacs build into pkgs
(self: super:
  let
    <<nix-linux-overlay-defs>>
  in
  {
    my-emacs = (pkgs.buildEnv {
      name = "my-emacs";
      paths = [
        <<nix-linux-overlay-emacs-paths>>
      ];
    });
  })

We bundle emacs through a bunch of helpers that set whatever flags we need set and also take case of factoring our wanted package repositories into the mix.

🤔 Looking at the NixOS Emacs wiki entry, it may be worthwhile to revaluate what the difference between emacsGit, used in my GNU/Linux config, and emacsPgtkGcc (now known as emacsPgtkNativeComp) really is.

emacs = (pkgs.emacs-unstable.override {
  withNativeCompilation = true;
  withSQLite3 = true;
  withGTK2 = false;
  withGTK3 = false;
  withTreeSitter = true;
});
bundled-emacs = emacs.pkgs.withPackages (epkgs: (
  with epkgs; [
    notmuch
    vterm
    pdf-tools
  ]
));

We define the paths for our custom emacs build seperately below:

bundled-emacs
pkgs.clang
pkgs.cmake
pkgs.coreutils
pkgs.fd
pkgs.multimarkdown
pkgs.python39

Darwin overlay <<nix-overlays-darwin>>

🐉 This has not been sufficiently tested so use with caution. I’m using NixOS mainly, so anything Darwin-related has probably been run once on a MacBook that I haven’t touched for a good 4 months at the time of writing (being [2022-06-30 Thu 18:31]).

For simplicity’s sake, we’re just assigning the standard v29 MacPort of Emacs in order to get things started.

# Overlay custom Emacs build into pkgs
(self: super: {
  my-emacs = pkgs.emacs29-macport.pkgs.withPackages (epkgs: (
    with epkgs; [
      notmuch
      vterm
      pdf-tools
    ]
  ));
})

In order to resolve an issue with Spotlight’s ability to find Emacs, remember to also install Emacs as a system package in order to set up the links in the macOS Application directory.

Packages

mu

When using home-manager, the runPersonalMuInit activation executes mu init with the correct parameters in order to correctly setup your mu index. In case you want to retrigger this activation, the ~/.cache/mu directory must not exist. I typically rename ~/.cache/mu to ~/.cache/mu-YYYYMMDD in order to meet the condition to trigger a mu init.

mu: basic package
mu
Explore if we can drop the overriden package instead of basic

The overriden package may no longer be necessary at this point. Tried to build previously with this “plain” mu and that worked so I think we’re ready to move along and park the overriden stuff.

Shorthand to emacsclient

(writeScriptBin "e" ''
  exec emacsclient -a emacs -c "$@"
'')

Emacs Org-Protocol

https://orgmode.org/worg/org-contrib/org-protocol.html

baseCommand = windowName:
  builtins.concatStringsSep " " [
    "emacsclient -a emacs"
    ''-F "((name . \\\"${windowName}\\\"))"''
    "-c"
  ];
(makeDesktopItem {
  name = "emacs-org-protocol";
  exec = "${(baseCommand "emacs-org-protocol")} %u";
  comment = "Org Protocol";
  desktopName = "org-protocol";
  categories = [
    "Utility"
    "Database"
    "TextTools"
    "TextEditor"
    "Office"
  ];
  mimeTypes = [
    "x-scheme-handler/org-protocol"
  ];
  terminal = false;
})
Bookmarks
javascript:location.href = 'org-protocol://sub-protocol://' + encodeURIComponent(location.href) + '/' + encodeURIComponent(document.title) + '/' + encodeURIComponent(window.getSelection())

https://www.reddit.com/r/firefox/comments/k64ha0/fix_allow_this_site_to_open_the_protocol_link/

Store link
javascript:location.href='org-protocol://store-link://'+encodeURIComponent(location.href)
Capture
javascript:location.href = 'org-protocol://capture://' + encodeURIComponent(location.href) + '/' + encodeURIComponent(document.title) + '/' + encodeURIComponent(window.getSelection())

Emacs mu4e

<<nix-linux-packages-desktop-emacs-mu4e>>

<<nix-linux-packages-wrapper-emacs-mu4e>>
# https://www.emacswiki.org/emacs/MailtoHandler
# https://dev.spacekookie.de/kookie/nomicon/commit/9e5896496cfd5da5754018887f7ad3b256b3ad80.diff
(makeDesktopItem {
  name = "emacs-mu4e";
  exec = "emacs-mu4e %u";
  comment = "Emacs mu4e";
  desktopName = "emacs-mu4e";
  type = "Application";
  categories = [
    "Network"
    "Email"
  ];
  mimeTypes = [
    # Email
    "x-scheme-handler/mailto"
    "message/rfc822"
  ];
  terminal = false;
})
(writeScriptBin "emacs-mu4e" ''
  set -e
  target_path=$@
  echo "Target: $target_path"

  exec emacsclient -a emacs -c -F "((name . \"emacs-mu4e\"))" -e "(vidbina-mime-handle-open-message-in-mu4e \"emacs-mu4e\" \"$target_path\")"
'')

Dired

<<nix-linux-packages-desktop-emacs-dired>>

<<nix-linux-packages-wrapper-emacs-dired>>
# https://emacs.stackexchange.com/questions/13927/how-to-set-emacs-as-the-default-file-manager
(makeDesktopItem {
  name = "emacs-dired";
  exec = "emacs-dired %f";
  comment = "Emacs Dired";
  desktopName = "emacs-dired";
  categories = [
    "Utility"
    "FileManager"
    "FileTools"
  ];
  mimeTypes = [
    "inode/directory"
    "inode/symlink"
  ];
  terminal = false;
})
(writeScriptBin "emacs-dired" ''
  set -e
  target_path=$(printf '%q\n' "$@" | xargs realpath -e)
  echo "Sanitized target to: $target_path"

  exec emacsclient -a emacs -c -F "((name . \"emacs-dired\"))" -e "(vidbina-mime-handle-open-directory \"emacs-dired\" \"$target_path\")"
'')

Python

python312

ripgrep

We build ripgrep for Doom Emacs. I haven’t even used Doom Emacs for a long time but the setting is still here and perhaps I can rely on this again for a faster grepping experience:

ripgrep-for-doom-emacs = (pkgs.ripgrep.override {
  withPCRE2 = true;
});

We also add the custom ripgrep to our Emacs paths to factor it into the build:

ripgrep-for-doom-emacs

only Darwin, the standard ripgrep is used until we find better reasons to invest in finding a different variant of ripgrep.

ripgrep

XDG mimeApps

xdg.mimeApps.defaultApplications = {
  "inode/directory" = [ "emacs-dired.desktop" ];
  "inode/symlink" = [ "emacs-dired.desktop" ];

  "message/rfc822" = [ "emacs-mu4e.desktop" ];
  "x-scheme-handler/mailto" = [ "emacs-mu4e.desktop" ];

  "x-scheme-handler/org-protocol" = [ "emacs-org-protocol.desktop" ];
};

Service configuration

GNU/Linux systemd Emacs service

On GNU/Linux 🐧, home-manager allows for the installation of the Linux packages, configuration of systemd services, configuration of overlays and registration of XDG mimeApps entries in order to produce a “working environment”. The complete GNU/Linux configuration looks as follows but is explained in the following subsections:

programs.emacs = {
  enable = true;
  package = pkgs.my-emacs;
};

For GNU/Linux, we can use the home-manager services.emacs attribute to enable the Emacs service:

services.emacs = {
  # Restart using `systemctl --user restart emacs`
  enable = true;
  package = pkgs.my-emacs;

  client.enable = true;
};

Setup Emacs configuration directory

  • State “TODO” from [2023-10-04 Wed 16:07]
    Fix mkOutOfStoreSymlink usage as the relative link used to work in my pre-Flake setup but is broken now.

We instruct home-manager to symlink the current directory into the ~/.emacs.d destination.

home.file.".emacs.d".source = config.lib.file.mkOutOfStoreSymlink ./.;

Footnotes

[fn:6] Different variants of Markdown may have slightly differing and sometimes conflicting notation for some simple formatting markers such as the ones needed to underline, boldface or italicize text.

[fn:4] By allowing the Emacs package system to load packages prior to engaging our selected package manager, it becomes harder to establish where package-related state gets introduced. By choosing to manage all packages declaratively (through code) through a single package manager, one creates a situation that is easier to debug (single actor to observe) and reproduce (by reevaluating the configuration) while being a tad more deterministic (reduced ability of imperatively grown global state to break the configuration).

[fn:3] Remember that we’re just using ~/.emacs.d to simplify the text, but if you use another emacs configuration directory you’ll need to substitute every occurence of that path accordingly)

[fn:2] I am the perpetual beginner 🌱 so I’m mostly writing this for my future self. 😅

[fn:1] Literal in the “Literal Programming” way as coined by Donald Knuth. There are a bunch of interesting bytes on the topic at literateprogramming.com and numerous other resources that can be link-followed when starting from one of the more recent related HN threads as per [2022-06-30 Thu] to the article Donald Knuth was framed (2020).

[fn:5] Keywords can be recognized by the : (colon character) prefix.

Custom Keyboard Helpers

mugur

  • State “CANCELED” from “WIP” [2023-07-04 Tue 14:10]
    Bricked my boards. No need to sweat this for now.
;; https://github.com/mihaiolteanu/mugur
(use-package mugur
  :straight (mugur :type git
                   :host github
                   :repo "mihaiolteanu/mugur"))
;;(advice-add 'mugur--symbol :after 'mugur--symbol)
;;(advice-remove 'mugur--symbol)
(advice-add 'mugur--keycode
            :filter-return
            (lambda (x)
              (pcase x
                ("KC_BSLASH" "KC_BSLS")
                ("KC_BSPACE" "KC_BSPC")
                ("KC_LAPO" "SC_LAPO")
                ("KC_LBRACKET" "KC_LBRC")
                ("KC_RAPC" "SC_RAPC")
                ("KC_RBRACKET" "KC_RBRC")
                ("KC_SCOLON" "KC_SCLN")
                ("RESET" "QK_BOOT")
                (_ x)))
            '((name . "blah")))
(advice-remove 'mugur--keycode "H")

(let ((/m "blah")) (message /m))

(vidbina/kill (mugur--keycode '(G ent)))

Example of a keymap based off of https://github.com/zzkt/charybdis which is based off of https://github.com/zzkt/crkbd:

(let ((mugur-qmk-path        "~/src/vidbina/qmk_firmware")
      (mugur-keyboard-name   "bastardkb/charybdis/4x6")
      (mugur-layout-name     "LAYOUT")
      (mugur-keymap-name     "mugur-emacs-vidbina")
      (mugur-user-defined-keys '((email  "[email protected]")
                                 (replay (C-x e))
                                 (E (MO emacs))
                                 (z_ (LT mouse z))
                                 (/_ (LT mouse slash)))))
  (mugur-mugur '(
     ("base"
       esc  1     2   3   4   5          6   7   8   9     0  bspace
       tab  q     w   e   r   t          y   u   i   o     p  -
       S    a     s   d   f   g          h   j   k   l    ";" (LT move ?\')
       C    z_    x   c   v   b          n   m  "," "."   /_  (LT hypm down)

                M  space  (G spc)       (TT numeric)  (G ent)
                       E     lapo       rapc)

     ("numeric"
       "~"       ?\!   ?\@   ?\#  ?\$  ?\%     ?\^  ?\&  ?\*   _   +  bspace
       tab       ?\!   ?\@   ?\#  ?\$  ?\%     ?\^  ?\&  ?\*   -   =  ---
        0         1     2     3    4    5       6    7    8    9   0  (LT move ent)
       (S left)  "`"   ?\\   ?\\  ?\{  ?\[     ?\]  ?\} comma dot  |  (S right)

                               --- --- ---   --- ent
                            (TG mouse) lapo   rapc)

     ("move"
       --- M-<   ---  ---  --- ---      ---  ---  ---  ---  --- ---
       --- M-v   up   ---  --- ---      ---  ---  ---  ---  --- ---
       C-a left down right C-e ---      left  up  down right -x- ---
       --- M-<  C-v   M->  --- ---      ---  ---  ---  ---  --- ---

                      C    S   ---      ---  ---
                           --- ---      ---)

     ("emacs"
       esc  --- --- (C-x 0) (C-x 2) (C-x 3)      (C-x 4 t)  --- --- --- --- ---
       ---  --- --- (C-x 0) (C-x 2) (C-x 3)      (C-x 4 t)  --- --- (C-M o) --- ---
       ---  --- M-%   ---     ---    (H-t)       (C-x b)    --- --- "λ" --- ---
      reset --- M-x   C-c     ---      ?\(        ?\)  (M-x "magit" ent) --- --- --- ---

                           ---  ---  (H-i e)   (C-x 8) (MO hypm)
                                ---    ---      ---)

     ("hypm"
         x  --- --- --- --- ( C-a "* " )    ---     ---    ---      ---     ---   ---
         x  --- --- --- ---  "  - [ ] "     ---     ---     H-i   (H-i o) (H-i l) ---
        --- --- --- H-d ---  "  - "         ---     ---     ---     ---     ---   reset
        --- --- --- --- ---    ---          (H-m n) (H-m m) (H-m s) ---     ---   ---

                       --- --- ---      ---  ---
                          ---  ---      ---)

     ("mouse"
       --- --- --- --- --- ---                         --- --- --- --- --- ---
       --- "SNIPER_CONFIG" --- --- --- ---             --- --- --- --- --- ---
       --- "DPI_CONFIG" "DRAG_SCROLL" --- --- ---      --- --- "DRAG_SCROLL" --- --- ---
       --- --- --- --- --- ---                         --- btn1 btn2 btn3 --- ---
                                    btn2 btn1 G     --- ---
                                            C S     ---)
     )))

Local Variables

For convenience, we call delete-trailing-whitespace as outlined in an emacs-orgmode mailing thread to automatically clean up trailing whitespaces that may be artifact from tangling noweb refs that

  1. contain line-breaks and are being indented or
  2. have no noweb-ref writes

;;; Local Variables: ;;; eval: (add-hook ‘org-babel-post-tangle-hook #’delete-trailing-whitespace) ;;; eval: (add-hook ‘org-babel-post-tangle-hook #’save-buffer :append) ;;; End: