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).
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.
The my-dotfiles-tangle-wrapper
helper allows us to spawn an Emacs session with our freshly tangled configuration in order to spot early errors.
(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 the tangle helper by running the following snippet with (org-ctrl-c-ctrl-c)
mapped by default to C-c C-c
.
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 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)
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:
- clone this repo to a path of choice (e.g.:
~/src/THIS_REPO
) - to install either
- my entire config:
- enter root of the repo and
- run
make install
- or just my Emacs-related config:
- borrow ideas from default.nix (or default-darwin.nix for macOS users) and place them into your own home-manager configuration
- my entire config:
- 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
<<emacsconfdir>>
~/.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:
- clone this repo to a path of choice (e.g.:
~/src/THIS_REPO
) - symlink
~/.emacs.d=[fn:3] to the =emacs
subdirectory of the cloned project (e.g.:ln -s ~/src/THIS_REPO/emacs ~/.emacs.d
) - optionally, populate
~/.emacs.d/lang.el
or~/.emacs.d/personal.el
(refer to Usage for instructions) - 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)
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.
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)
For convenience’s sake, let’s start stubbing some of the files that we will be tangling from this literate configuration.
;; Tangled from dotfiles/emacs/README.org
<<tangle-providence>>
<<tangle-providence>>
<<tangle-providence>>
<<tangle-providence>>
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")
We enabe lexical binding, since some packages (e.g.: consult) will require this.
;; -*- lexical-binding: t -*-
(message "🚜 Loading init.el")
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.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).
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:
- straight
- org-contrib
- dockerfile-mode
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))
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)))
On the tidying up front, for example, you’ll find that the use-package macro provides mechanisms to:
- order the load sequence of packages by defining dependencies by means of the
:after
keyword - 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 - bind keychords using the
:bind
keyword
On the performance front, you’ll find that the use-package macro provides mechanisms to:
- delay loading of packages by means of the
:defer
or:demand
keywords - delay loading of packages needed in a particular mode or interpreter by means of the
:mode
or:interpreter
keywords - define “as-of-yet not seen” symbols that will be needed for compilation by means of the
:functions
and:defines
keywords
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.
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)
(with-eval-after-load 'eldoc
(setq eldoc-echo-area-prefer-doc-buffer t))
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:
- 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
- natively allows for the notation of dates and times which allow for things like time-tracking and planning within a single document.
- 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)
:after magit
: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>>)
(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")
(org-format-latex-options '(
:foreground default
:background default
:scale 4.0
:html-foreground "Black"
:html-background "Transparent"
:html-scale 1.0
:matchers ("begin" "$1" "$" "$$" "\\(" "\\[")))
Variable org-format-latex-options
has the :scale
attribute which informs how large LaTeX is rendered.
(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)))
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
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.
- State “PROTOTYPE” from “WIP” [2022-06-28 Tue 13:34]
initial concept usingfind-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)))
Is there something here that we can borrow to bring our implementation more in line with existing ways to handle async export.
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.
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)>>")
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.
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)>>;
}
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.
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")
(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
)
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.
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)
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")))
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))
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, potentially | Feature Column | Another Feature Column | Yes another feature column |
---|---|---|---|
Just to test that this works |
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}
}
;; 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)
A good solution for maintaining a Zettelkasten-inspired note-taking system is Org-Roam 🗄️ which allows one to conveniently link related notes together.
;; 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>>))
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
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"))
Set the Org-roam database location:
(setq org-roam-db-location (file-truename "~/org/roam/org-roam.db"))
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
- minimize issues with sqlite (which isn’t a multi-connection database) and to
- 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)
;; 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))
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))))
;; 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
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.
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.
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"))
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
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.
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>>))
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).
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)
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")
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)))
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))
The modeline is the bar typically at the bottom of a buffer which provides useful information about the system.
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.
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 "♻️")
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"))
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)
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.
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).
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)))))
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.
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 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"))
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.
;; https://gitlab.com/protesilaos/modus-themes
(use-package modus-themes
:straight (modus-themes :type git
:host gitlab
:repo "protesilaos/modus-themes")
:custom
<<modus-custom>>
)
(modus-themes-bold-constructs t)
(modus-themes-org-blocks 'gray-background)
(modus-themes-prompts '(intense))
(modus-themes-headings '((0 . (light 1.5))
(1 . (regular 1.3))
(2 . (regular 1.1))
(t . (regular 1.1))))
(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)))
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)
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)))))
;; 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
.
;; https://github.com/joaotavora/yasnippet
(use-package yasnippet
:straight (yasnippet :type git
:host github
:repo "joaotavora/yasnippet")
:config
(yas-global-mode 1))
(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
`())))))
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")
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>>)
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)))
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)
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)
This run was particularly problematic, as you can tell it has basically consumed a crapload of memory and GC seems to be going wild.
Memory7,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)
Point on README.org and trying to M-o
to Help bufer with org-html-head-include-default-style
.
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>
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.
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% + ...
Let’s study what avy-tree
really does. The signature is (avy-read TREE DISPLAY-FN CLEANUP-FN)
and is defined in avy.el.
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)))
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"))
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))))
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. 🤷🏿♂️
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.
;; https://github.com/emacsmirror/undo-fu
(use-package undo-fu
:straight (undo-fu :type git
:host github
:repo "emacsmirror/undo-fu"))
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)
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.
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.
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))
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)
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.
Disabling magit in order to fall back on the Emacs default.
;; https://github.com/magit/magit.git
(use-package magit
:straight (magit :type git
:host github
:repo "magit/magit")
:ensure t
:bind (("C-x g" . magit-status)
("C-x C-g" . magit-status))
:custom
(with-editor-emacsclient-executable nil)
(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"))
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.
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:
Forge | machine | user |
---|---|---|
GitHub | api.github.com | vidbina^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
⛔ 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)
The thread at radian-software/straight.el#326 outlines that this issue is caused by us installing a package at a given version while a tightly coupled dependency is installed at a different version, thus creation a situation of incompatibility. My hunch is that this issue is caused by attempts to custom install magit without installing a matching magit-section or git-commit.
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"))
;; 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))
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)))
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)))
(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")))
;; (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")
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.
The orderless package provides more generous completion resolution by permitting us to:
- provide partial phrases e.g.: “o i d” to filter for “org-indent-drawer” and
- 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
<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 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 💊"))
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
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).
The which-key package annotes the command listing with the key bindings for the shown commands.
;; 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))
(use-package vterm
:straight (:type built-in)
:after evil
:init (evil-collection-vterm-setup)
:hook
<<vterm-hooks>>
:config
<<vterm-config>>)
Just type ab, navigate to beginning of word and enter dw
to see what happens.
Produces an execute: _
prompt where I suppose we get to enter vterm-related shell commands for execution. What is the deal here.
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)
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"))))
Running magic-status
in vterm doesn’t work because variable magit-repository-directory
.
(message magit-repository-directories)
In vterm:
- Enter a word after the prompt
- selecting that word
- Enter
dw
evil binding (to delete word)
Result: triggers a case change.
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"))
(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))
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)))
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.
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):
- How does projectile compare to the built-in project.el? (reddit)
- It’s never too late (Manuel Uberti)
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
- most generally, defining new project types or
- 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)
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.
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))
(use-package auth-source
:straight (:type built-in)
:config
<<auth-source-config>>)
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.
;; https://github.com/alpha22jp/atomic-chrome
(use-package atomic-chrome
:straight (atomic-chrome :type git
:host github
:repo "alpha22jp/atomic-chrome"))
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.
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.
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
Some of the functions that are relevant:
- sendmail-send-it
- feedmail-send-it
- smtpmail-send-it
- mailclient-send-it
*message-smtpmail-send-it*callssmtpmail-send-it
after evaluatingmessage-send-mail-hook
but is obsolete and message-use-send-mail-function is recommended instead which does almost the same (runningmessage-send-mail-hook
and then firingsend-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.
;; 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")))
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")
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")
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.
(use-package message
:straight (:type built-in)
:custom
<<message-custom>>)
We set the mail directory:
(message-directory "~/mail/")
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")
(message-sendmail-f-is-evil t "Avoid setting -f (--from) when calling sendmail")
(message-sendmail-envelope-from 'header "Use From: header")
(message-kill-buffer-on-exit t "Kill a buffer once a message is sent")
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)
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)
💡 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 topersonal.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 toC-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")))
(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
.
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)
* TODO %?
%i
%a
(list "t" "Todo" 'entry
'(file+headline "~/org/todo.org" "Tasks")
`(file ,(expand-file-name "templates/todo.org")))
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)
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)
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)
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 `())
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")))
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).
(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-param-set (mode-line-color wallpaper-color emacs-theme)
(setq zoom-window-mode-line-color mode-line-color)
(load-theme emacs-theme :no-confirm)
(vidbina/theme-switch-update-cursor-styles)
(pcase system-type
(darwin (error "No hsetroot on Darwin"))
('gnu/linux (async-shell-command (format "hsetroot -solid '%s'" "*dotfile-helpers*"
wallpaper-color))
(message "see *dotfile-helpers* for output of wallpaper color set"))
(_ (error "Skipping wallpaper color set on unknown sys"))))
(defun vidbina/theme-switch-to-dark ()
"Switch to the dark theme"
(interactive)
(vidbina/theme-param-set "DodgerBlue4" "#000000" 'modus-vivendi)
(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)
(vidbina/theme-param-set "Gold" "#ff9800" 'modus-operandi)
(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)
(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)))
(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))
(defun vidbina/browse-url-xsel (url &optional ignored)
(message "🌐 Browse: copy url [%s]" url)
(kill-new 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 "🌐 Browse: open file [%s]" buffer-file-name)
(browse-url (file-truename buffer-file-name))))))
(add-hook 'after-save-hook 'vidbina/browse-to-current-file)
(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)
(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)
(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)))
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:
- it opens up a new frame (leaving existing frames as-is)
- 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 theemacsclient
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)"
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))
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))))
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")))
(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)))
(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)
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)))
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)
(global-set-key (kbd "C-c v z") 'zoom-window-zoom)
(global-set-key (kbd "C-c v j") 'flymake-show-buffer-diagnostics)
(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "C-c a") 'org-agenda)
(setq fill-column 1)
(setq whitespace-style '(trailing tabs newline tab-mark newline-mark))
;; 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
;; 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)
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)
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
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 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:
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. 🤔
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! 🤷🏿♂️
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).
;; 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")
(ispell-dictionary "en_US,de_DE,nl_NL,fr-toutesvariantes,es_ANY")
(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)))
;; 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)))))
(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">
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 withjson-reformat
c-c P
copy path to object at point to the kill ring
(json-mode . json-ts-mode)
(json . ("https://github.com/tree-sitter/tree-sitter-json"))
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 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"))
;; https://github.com/tminor/jsonnet-mode
(use-package jsonnet-mode
:straight (jsonnet-mode :type git
:host github
:repo "tminor/jsonnet-mode"))
;; https://github.com/yoshiki/yaml-mode
(use-package yaml-mode
:straight (yaml-mode :type git
:host github
:repo "yoshiki/yaml-mode"))
(yaml-mode . yaml-ts-mode)
yamllint
We install the YAML language server to offer LSP support for YAML files.
nodePackages.yaml-language-server
;; 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)))))
;; 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)))))
;; 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)))))
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
|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
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. 🤷🏿♂️
(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)))))
;; https://github.com/spotify/dockerfile-mode
(use-package dockerfile-mode
:straight (dockerfile-mode :type git
:host github
:repo "spotify/dockerfile-mode"))
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)))))
;; https://github.com/NixOS/nix-mode
(use-package nix-mode
:straight (nix-mode :type git
:host github
:repo "NixOS/nix-mode")
:after magit
:custom
(nix-nixfmt-bin "nixpkgs-fmt"))
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.
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.
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
- https://www.reddit.com/r/NixOS/comments/r15hx4/nix_shell_vs_nix_develop/
- https://blog.ysndr.de/posts/guides/2021-12-01-nix-shells/
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.
Theorg-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:
- when
stdin
orcmdline
are defined → external shell script w/ stdin - when
session
is defined → session evaluation - if
shebang
is not empty, external shell script w/ or w/o shebang - 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"
The nix-develop prompt will either default to “> ” or the value of the nixConfig.bash-prompt
attribute.
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)
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)))))))))
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))
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
<<nix-develop-mode>>
<<nix-develop-ob-execute>>
(provide 'nix-develop-mode)
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
;; https://github.com/fxbois/web-mode
(use-package web-mode
:straight (web-mode :type git
:host github
:repo "fxbois/web-mode"))
;; https://github.com/fxbois/web-mode
(use-package web-mode
:straight (web-mode :type git
:host github
:repo "fxbois/web-mode"))
(python-mode . python-ts-mode)
(python . ("https://github.com/tree-sitter/tree-sitter-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"))))
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)))))
;; https://github.com/dominikh/go-mode.el
(use-package go-mode
:straight (go-mode :type git
:host github
:repo "dominikh/go-mode.el"))
(go . ("https://github.com/tree-sitter/tree-sitter-go"))
(javascript "https://github.com/tree-sitter/tree-sitter-javascript")
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))
Superset of JavaScript that folks really should be using instead of just vanilla JS but we’re not here to judge. 🤷🏿♂️
Note that Emacs not bundles typescript-ts-mode
so we may not need to install a separate package for this. As per <2024-10-22 Tue>, we are commenting-out all custom config regarding TypeScript.
(typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src"))
(tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src"))
;; https://github.com/gleam-lang/gleam-mode
(use-package gleam-ts-mode
:straight (gleam-ts-mode :type git
:host github
:repo "gleam-lang/gleam-mode"))
For treesitter, we install the necessary grammar as mentioned in the PR listing the official Gleam grammar:
(gleam . ("https://github.com/gleam-lang/tree-sitter-gleam"))
For the ease of use, we configure the Gleam-specific bit for major-mode-remap-alist
:
(gleam-mode . gleam-ts-mode)
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"))
(lua . ("https://github.com/Azganoth/tree-sitter-lua"))
;; https://github.com/mcandre/vimrc-mode
(use-package vimrc-mode
:straight (vimrc-mode :type git
:host github
:repo "mcandre/vimrc-mode"))
;; 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.
;; 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"))
;; 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"))
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 . ("https://github.com/tree-sitter/tree-sitter-haskell"))
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
.
;; https://github.com/jcollard/elm-mode
(use-package elm-mode
:straight (elm-mode :type git
:host github
:repo "jcollard/elm-mode"))
(elm . ("https://github.com/elm-tooling/tree-sitter-elm"))
;; https://github.com/rust-lang/rust-mode
(use-package rust-mode
:straight (rust-mode :type git
:host github
:repo "rust-lang/rust-mode"))
(rust . ("https://github.com/tree-sitter/tree-sitter-rust"))
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 bells and whistles from formatting aids to LLM-enabled magic.
;; 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 "🎛️"))
See editorconfig/editorconfig-emacs#264 regarding indent_size
being ignored editorconfig is being ignored.
;; 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)
)
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)
⚠️ [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, and after some sleuthing hich landed on 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 fit 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
See https://emacs-tree-sitter.github.io/getting-started/ for instructions on how to get started with tree-sitter.
(use-package treesit
:straight (:type built-in)
:init
(setq treesit-language-source-alist
'(
<<treesit-languages>>
))
:config
<<treesit-config>>
(defun vidbina/treesit-install-all-languages ()
"Install all languages specified by `treesit-language-source-alist'."
(interactive)
;; https://www.masteringemacs.org/article/how-to-get-started-tree-sitter
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist)))
(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)))))
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. 🤷🏿♂️
We define the major mode remap assoc list in order to tighten the treesitter config:
(setq major-mode-remap-alist
'(
<<treesit-config-major-mode-remap>>
))
Individual language settings are tangled into our setq
above in order to simplify splitting of this part of the config across the language-specific section. We do this for the first time for the Gleam setup.
(bash-mode . bash-ts-mode)
(js2-mode . js-ts-mode)
(css-mode . css-ts-mode)
(add-to-list 'auto-mode-alist '("\\.[tj]sx?\\'" . tsx-ts-mode))
See a listing of available grammars at https://tree-sitter.github.io/tree-sitter/ with links to their respective repositories.
Just because treesit-language-source-alist
needs this (for the treesit config), we define the language to grammar repo mapping here for “reuse”. See Mastering Emacs for examples on how to set this up.
(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"))
(html . ("https://github.com/tree-sitter/tree-sitter-html"))
(make . ("https://github.com/alemuller/tree-sitter-make"))
(org . ("https://github.com/milisims/tree-sitter-org"))
(sql . ("https://github.com/m-novikov/tree-sitter-sql"))
(toml . ("https://github.com/tree-sitter/tree-sitter-toml"))
The general rule-of-thumb is that a treesitter repo should contain a folder with a grammar.c file. We generally point our configuration to the repo, branch and directory that contains the standard treesitter filetree. For some repos, we may need to specify a subdirectory in the repo to find files such as grammar.js or grammar.json, parser.c and the other main treesitter files.
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)
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 allows us to treat structural units of code with specific actions like movement, splicing and selection.
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"))
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 "👨🏿🏭"))
;; 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))
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)))
:delight
(paredit-mode "🛝")
Enable paredit mode using the C-c v (
binding.
:bind (("C-c v (" . paredit-mode))
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)}
- with point before
C-M-b
/C-M-f
move backward/forwardC-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)
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"))
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.
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://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/david@asabina.de/api-key-2023.04.18-emacs-vidbina")))
:custom
<<gptel-custom>>
)
(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.")
))
(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)
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.
https://list.orgmode.org/87zh2g2pby.fsf@bzg.fr/t/
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.
- 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.
- 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>>
}
# Tangled from README.org
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
<<nix-packages>>
<<nix-darwin-packages>>
];
nixpkgs.overlays = [
<<nix-darwin-overlays>>
];
<<nix-darwin>>
}
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
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
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))
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
🐉 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.
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
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.
(writeScriptBin "e" ''
exec emacsclient -a emacs -c "$@"
'')
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;
})
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/
javascript:location.href='org-protocol://store-link://'+encodeURIComponent(location.href)
javascript:location.href = 'org-protocol://capture://' + encodeURIComponent(location.href) + '/' + encodeURIComponent(document.title) + '/' + encodeURIComponent(window.getSelection())
<<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\")"
'')
python312
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
In order to deal with the following type of errors:
⛔ Warning (comp): libgccjit.so: error: error invoking gcc driver
we try to install libgccjit into Emacs:
libgccjit
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" ];
};
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;
};
- 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 ./.;
[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.
- 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 "mihai@fastmail.fm")
(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 ---)
)))
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
- contain line-breaks and are being indented or
- have no noweb-ref writes
(message "🏁 End of the config")
;;; 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: