This is my reproducible Emacs configuration on top of Nix and straight.el. Nix takes care of any system-level dependencies and downloading an initial version of straight.el which then installs and manages itself and other Emacs Lisp packages.
You can try the config, e.g., inside Docker by running the following steps:
docker run -it nixos/nix /bin/sh
nix-shell -p emacs-nox wget
wget https://raw.githubusercontent.com/kinnala/nixemacs/master/readme.org
emacs --file readme.org --eval '(progn (org-babel-tangle) (kill-emacs))'
exit
nix-env -i tom
emacs
The initial version of straight.el is patched slightly so that it doesn’t perform any HTTP requests while bootstrapping. All requests to straight.el GitHub repo are replaced by file reads to the Nix store.
{ stdenv, fetchFromGitHub }:
stdenv.mkDerivation rec {
name = "straight.el";
src = fetchFromGitHub {
owner = "raxod502";
repo = "straight.el";
rev = "59c92dd45085b8f8fc44ea0039c205f4a3c43b62";
sha256 = "00ibxmgqfb5bqd4b9jqj8vdiszkph6vv64m1y3kf9xav15v8sfyx";
};
buildPhase = ''
sed -i -e 's|(with-current-buffer|(with-temp-buffer|g' install.el
sed -i -e 's|(url-retrieve-synchronously|(insert-file-contents "'"$out"'/share/straight/straight.el")|g' install.el
sed -i -e 's| (format||g' install.el
sed -i -e 's|(concat "https:\/\/raw.githubusercontent.com\/"||g' install.el
sed -i -e 's|"raxod502\/straight.el\/install\/%s\/straight.el")||g' install.el
sed -i -e 's|(substring (symbol-name version) 1))||g' install.el
sed -i -e "s|'silent 'inhibit-cookies)||g" install.el
sed -i -e "s|(unless (equal url-http-response-status 200)||g" install.el
sed -i -e 's|(error "Unknown recipe version: %S" version))||g' install.el
sed -i -e "s|(delete-region (point-min) url-http-end-of-headers)||g" install.el
echo '(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-temp-buffer
(insert-file-contents "'"$out"'/share/straight/install.el")
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil '"'"'nomessage))' > init.el
echo -e '#!/bin/bash\necho -n "'"$out"'/share/straight/init.el"' > straight-init-path
'';
installPhase = ''
mkdir -p $out/share/straight
cp install.el straight.el init.el $out/share/straight/
mkdir -p $out/bin
cp straight-init-path $out/bin/
chmod +x $out/bin/straight-init-path
'';
}
This uses the above derivation and calls buildEnv
for creating a set of
packages.
self: super:
{
straight.el = super.callPackage (import ../straight/default.nix) {};
tom = super.buildEnv {
name = "tom";
paths = [
self.emacs
self.straight.el
self.ripgrep
self.gnumake
self.pass
];
};
}
This is a version lockfile for straight.el.
(("Emacs-wgrep" . "f9687c28bbc2e84f87a479b6ce04407bb97cfb23")
("Highlight-Indentation-for-Emacs" . "d88db4248882da2d4316e76ed673b4ac1fa99ce3")
("csv-mode" . "53beddc207864b0c3f81da85b59245dff8eea5ce")
("dash.el" . "76606f90774c5349f7adac20c33e6d37a1939a1f")
("dired-k" . "1ddd8e0ea06f0e25cd5dedb2370cfa0cacfa8c9d")
("diredfl" . "62b559e1d6b69834a56a57eb1832ac6ad4d2e5d0")
("dumb-jump" . "1dd583011f4025b1b8c75fd785691851b6c5dfa3")
("el-get" . "a620c91fe7d6d482c0e7538df75e10af0af1bb16")
("emacs-which-key" . "1ab1d0cc88843c9a614ed3226c5a1070e32e4823")
("emacsmirror-mirror" . "2e1ceba3c8036637832e99414d9012359911f5e4")
("expand-region.el" . "7e5bbe2763c12bae3e77fe0c49bcad05ff91dbfe")
("f.el" . "feb6c2cb9f8e19ebdfdfde0d89edbdd5d7f51a96")
("gnu-elpa-mirror" . "808923d95777d378ca340b8020dd571e6a62460a")
("hydra" . "9e9e00cb240ea1903ffd36a54956b3902c379d29")
("json-mode" . "eedb4560034f795a7950fa07016bd4347c368873")
("json-reformat" . "8eb6668ed447988aea06467ba8f42e1f2178246f")
("json-snatcher" . "b28d1c0670636da6db508d03872d96ffddbc10f2")
("magit" . "a7953b2645503904b2a31e18e019f07af9e71a7a")
("markdown-mode" . "4477f381de0068a04b55e198c32614793f67b38a")
("melpa" . "40c73fda1fc5fd5cf01680838a9556fb3fa528cf")
("moe-theme.el" . "edf3fe47fb986e283e3b04cba443dcb39fe8720e")
("multiple-cursors.el" . "fd8441bfc8738d463601823e5a3d9c2d7123bfbf")
("nix-mode" . "8fe2ccf0b01f694a77d2528e06c10f06057784f6")
("org" . "604bfd9d755770e12c368c15148780ec723211df")
("phi-search" . "c34f5800968922d1f9e7b10092b8705d6640ad18")
("popup-el" . "976bd8e06100df5a266377d0e5f63b104ba93119")
("rust-mode" . "0df2f22479b98f76d97de90e1c390ff1b0902a46")
("s.el" . "08661efb075d1c6b4fa812184c1e5e90c08795a9")
("straight.el" . "af5437f2afd00936c883124d6d3098721c2d306c")
("swiper" . "f8d80a4055514f92d94f128f5fcb1cda79e5cd22")
("transient" . "2e4426fe8161893382f09b3f4635e152ee02488e")
("use-package" . "a7422fb8ab1baee19adb2717b5b47b9c3812a84c")
("virtualenvwrapper.el" . "c7e84505db4142fd1beebf38ffe37c3f42444ca3")
("with-editor" . "4ab8c6148bb2698ff793d4a8acdbd8d0d642e133")
("xclip" . "4772beb5579e13910c89c482a2e41271253c646b")
("yaml-mode" . "535273d5a1eb76999d20afbcf4d9f056d8ffd2da"))
:beta
Note: to update, run straight-pull-all
and straight-freeze-versions
and replace above with the contents of ~/.emacs.d/straight/versions/default.el
.
The rest of the snippets are combined into Emacs init file. The first snippet
configures straight.el and runs a customized straight.el initialization script
from the Nix store (straight-init-path
returns path to the initialization
script):
(setq straight-use-package-by-default t)
(setq straight-vc-git-default-clone-depth 20)
(load-file (shell-command-to-string "straight-init-path"))
(straight-use-package 'use-package)
(use-package org
:commands org-babel-do-load-languages
:config
(unbind-key "C-," org-mode-map)
(unbind-key "C-." org-mode-map)
:init
(add-hook 'org-mode-hook (lambda ()
(fset 'tex-font-lock-suscript 'ignore)
(org-babel-do-load-languages
'org-babel-load-languages
'((python . t)
(shell . t)))))
(setq org-default-notes-file "~/Dropbox/Notes/agenda/inbox.org")
(setq org-agenda-files '("~/Dropbox/Notes/agenda/"))
(setq org-refile-targets '((nil :maxlevel . 9)
(org-agenda-files :maxlevel . 9)))
(setq org-log-done 'time)
(setq org-tags-column 0)
(setq org-agenda-tags-column 0)
(setq org-agenda-window-setup 'only-window)
(setq org-export-babel-evaluate nil)
(setq org-startup-folded nil)
(setq org-agenda-format-date
(lambda (date) (concat "\n"
(make-string (window-width) 9472)
"\n"
(org-agenda-format-date-aligned date))))
(setq org-agenda-skip-deadline-if-done t)
(setq org-deadline-warning-days 5)
(setq org-agenda-prefix-format '((agenda . " %i %-5e %-12:c%?-12t% s")
(todo . " %i %-12:c %b")
(tags . " %i %-12:c")
(search . " %i %-12:c")))
(setq org-adapt-indentation nil)
(setq org-refile-use-outline-path 'file)
(setq org-structure-template-alist '(("l" . "latex latex")
("s" . "src")))
(setq org-outline-path-complete-in-steps nil)
(setq org-src-preserve-indentation t)
(setq org-confirm-babel-evaluate nil)
(setq org-html-validation-link nil)
(setq org-babel-default-header-args:sh '((:prologue . "exec 2>&1")
(:epilogue . ":")))
(setq org-capture-templates '(("t" "Todo" entry
(file "~/Dropbox/Notes/agenda/inbox.org")
"* TODO %?\n SCHEDULED: %t\n%i\n%a")
("k" "Event" entry
(file "~/Dropbox/Notes/agenda/inbox.org")
"* %?\n%t")))
:bind (("C-c c" . org-capture)
("C-c a" . org-agenda)))
(use-package ivy
:commands
ivy-mode
:init
(ivy-mode 1)
(setq ivy-height 10
ivy-fixed-height-minibuffer t)
:bind (("C-x b" . ivy-switch-buffer)
("C-c r" . ivy-resume)
("C-x C-b" . ibuffer)))
(use-package counsel
:init
(setq counsel-find-file-ignore-regexp "\\archive\\'")
:bind (("M-x" . counsel-M-x)
("C-x C-f" . counsel-find-file)
("C-c g" . counsel-rg)
("C-c G" . counsel-git)
("C-c j" . counsel-file-jump)
("C-x b" . counsel-switch-buffer)
("C-c h" . counsel-minibuffer-history)
("M-y" . counsel-yank-pop)))
(use-package swiper
:bind ("C-c s" . swiper))
(use-package wgrep)
(use-package magit
:init
(setq magit-repository-directories '(("~/src" . 1)))
:bind (("C-x g" . magit-status)
("C-c M-g" . magit-file-dispatch)))
(use-package expand-region
:after (org)
:bind ("C-." . er/expand-region)
:init
(require 'expand-region)
(require 'cl)
(defun mark-around* (search-forward-char)
(let* ((expand-region-fast-keys-enabled nil)
(char (or search-forward-char
(char-to-string
(read-char "Mark inner, starting with:"))))
(q-char (regexp-quote char))
(starting-point (point)))
(when search-forward-char
(search-forward char (point-at-eol)))
(cl-flet ((message (&rest args) nil))
(er--expand-region-1)
(er--expand-region-1)
(while (and (not (= (point) (point-min)))
(not (looking-at q-char)))
(er--expand-region-1))
(er/expand-region -1))))
(defun mark-around ()
(interactive)
(mark-around* nil))
(define-key global-map (kbd "M-i") 'mark-around))
(use-package multiple-cursors
:init
(define-key global-map (kbd "C-'") 'mc-hide-unmatched-lines-mode)
(define-key global-map (kbd "C-,") 'mc/mark-next-like-this)
(define-key global-map (kbd "C-;") 'mc/mark-all-dwim)
(setq hum/lines-to-expand 1))
(use-package phi-search
:after multiple-cursors
:init (require 'phi-replace)
:bind ("C-:" . phi-replace)
:bind (:map mc/keymap
("C-s" . phi-search)
("C-r" . phi-search-backward)))
(use-package term
:straight nil)
(use-package dired-x
:straight nil)
(use-package dired
:straight nil
:after (term dired-x)
:init
(setq dired-dwim-target t)
(setq dired-omit-files "^\\...+$")
(defun run-gnome-terminal-here ()
(interactive)
(shell-command "kgx"))
(setq dired-guess-shell-alist-user
'(("\\.pdf\\'" "evince")
("\\.eps\\'" "evince")
("\\.jpe?g\\'" "eog")
("\\.png\\'" "eog")
("\\.gif\\'" "eog")
("\\.xpm\\'" "eog")))
:bind (("C-x C-j" . dired-jump))
:bind (:map dired-mode-map
("'" . run-gnome-terminal-here)
("j" . swiper)
("s" . swiper)))
(use-package dired-k
:after (dired)
:bind (:map dired-mode-map
("g" . dired-k)))
(use-package diredfl
:commands diredfl-global-mode
:init
(diredfl-global-mode)
(put 'diredp-tagged-autofile-name 'face-alias 'diredfl-tagged-autofile-name)
(put 'diredp-autofile-name 'face-alias 'diredfl-autofile-name)
(put 'diredp-ignored-file-name 'face-alias 'diredfl-ignored-file-name)
(put 'diredp-symlink 'face-alias 'diredfl-symlink)
(put 'diredp-compressed-file-name 'face-alias 'diredfl-compressed-file-name)
(put 'diredp-file-suffix 'face-alias 'diredfl-file-suffix)
(put 'diredp-compressed-extensions 'face-alias 'diredfl-compressed-extensions)
(put 'diredp-deletion 'face-alias 'diredfl-deletion)
(put 'diredp-deletion-file-name 'face-alias 'diredfl-deletion-file-name)
(put 'diredp-flag-mark-line 'face-alias 'diredfl-flag-mark-line)
(put 'diredp-rare-priv 'face-alias 'diredfl-rare-priv)
(put 'diredp-number 'face-alias 'diredfl-number)
(put 'diredp-exec-priv 'face-alias 'diredfl-exec-priv)
(put 'diredp-file-name 'face-alias 'diredfl-file-name)
(put 'diredp-dir-heading 'face-alias 'diredfl-dir-heading)
(put 'diredp-compressed-file-suffix 'face-alias 'diredfl-compressed-file-suffix)
(put 'diredp-flag-mark 'face-alias 'diredfl-flag-mark)
(put 'diredp-mode-set-explicitly 'face-alias 'diredfl-mode-set-explicitly)
(put 'diredp-executable-tag 'face-alias 'diredfl-executable-tag)
(put 'diredp-global-mode-hook 'face-alias 'diredfl-global-mode-hook)
(put 'diredp-ignore-compressed-flag 'face-alias 'diredfl-ignore-compressed-flag)
(put 'diredp-dir-priv 'face-alias 'diredfl-dir-priv)
(put 'diredp-date-time 'face-alias 'diredfl-date-time)
(put 'diredp-other-priv 'face-alias 'diredfl-other-priv)
(put 'diredp-no-priv 'face-alias 'diredfl-no-priv)
(put 'diredp-link-priv 'face-alias 'diredfl-link-priv)
(put 'diredp-write-priv 'face-alias 'diredfl-write-priv)
(put 'diredp-global-mode-buffers 'face-alias 'diredfl-global-mode-buffers)
(put 'dired-directory 'face-alias 'diredfl-dir-name)
(put 'diredp-read-priv 'face-alias 'diredfl-read-priv))
(use-package json-mode)
(use-package highlight-indentation
:commands (highlight-indentation-mode)
:init
(defun tom/hl-indent-color ()
(set-face-background
'highlight-indentation-face
(face-attribute 'highlight :background)))
(advice-add 'highlight-indentation-mode :after #'tom/hl-indent-color)
:hook ((python-mode . highlight-indentation-mode)
(python-mode . display-fill-column-indicator-mode)))
(use-package yaml-mode)
(use-package csv-mode
:mode "\\.csv$"
:init (setq csv-separators '(";")))
(use-package markdown-mode
:commands (markdown-mode)
:mode (("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode)))
(use-package nix-mode)
(use-package rust-mode)
(use-package moe-theme
:config
(load-theme 'moe-dark t))
(global-hl-line-mode)
(use-package dumb-jump
:config
(setq dumb-jump-selector 'ivy)
:init
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate))
(use-package virtualenvwrapper
:init (setq venv-location "~/.conda/envs"))
(use-package hydra)
;; from move-lines package, https://github.com/targzeta/move-lines
(defun move-lines--internal (n)
"Moves the current line or, if region is actives, the lines surrounding
region, of N lines. Down if N is positive, up if is negative"
(let* (text-start
text-end
(region-start (point))
(region-end region-start)
swap-point-mark
delete-latest-newline)
(when (region-active-p)
(if (> (point) (mark))
(setq region-start (mark))
(exchange-point-and-mark)
(setq swap-point-mark t
region-end (point))))
(end-of-line)
(if (< (point) (point-max))
(forward-char 1)
(setq delete-latest-newline t)
(insert-char ?\n))
(setq text-end (point)
region-end (- region-end text-end))
(goto-char region-start)
(beginning-of-line)
(setq text-start (point)
region-start (- region-start text-end))
(let ((text (delete-and-extract-region text-start text-end)))
(forward-line n)
(when (not (= (current-column) 0))
(insert-char ?\n)
(setq delete-latest-newline t))
(insert text))
(forward-char region-end)
(when delete-latest-newline
(save-excursion
(goto-char (point-max))
(delete-char -1)))
(when (region-active-p)
(setq deactivate-mark nil)
(set-mark (+ (point) (- region-start region-end)))
(if swap-point-mark
(exchange-point-and-mark)))))
(defun move-lines-up (n)
"Moves the current line or, if region is actives, the lines surrounding
region, up by N lines, or 1 line if N is nil."
(interactive "p")
(if (eq n nil)
(setq n 1))
(move-lines--internal (- n)))
(defun move-lines-down (n)
"Moves the current line or, if region is actives, the lines surrounding
region, down by N lines, or 1 line if N is nil."
(interactive "p")
(if (eq n nil)
(setq n 1))
(move-lines--internal n))
(defun tom/shift-left (start end &optional count)
"Shift region left and activate hydra."
(interactive
(if mark-active
(list (region-beginning) (region-end) current-prefix-arg)
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
(python-indent-shift-left start end count)
(tom/hydra-move-lines/body))
(defun tom/shift-right (start end &optional count)
"Shift region right and activate hydra."
(interactive
(if mark-active
(list (region-beginning) (region-end) current-prefix-arg)
(list (line-beginning-position) (line-end-position) current-prefix-arg)))
(python-indent-shift-right start end count)
(tom/hydra-move-lines/body))
(defun tom/move-lines-p ()
"Move lines up once and activate hydra."
(interactive)
(move-lines-up 1)
(tom/hydra-move-lines/body))
(defun tom/move-lines-n ()
"Move lines down once and activate hydra."
(interactive)
(move-lines-down 1)
(tom/hydra-move-lines/body))
(defhydra tom/hydra-move-lines ()
"Move one or multiple lines"
("n" move-lines-down "down")
("p" move-lines-up "up")
("<" python-indent-shift-left "left")
(">" python-indent-shift-right "right"))
(define-key global-map (kbd "C-c n") 'tom/move-lines-n)
(define-key global-map (kbd "C-c p") 'tom/move-lines-p)
(define-key global-map (kbd "C-c <") 'tom/shift-left)
(define-key global-map (kbd "C-c >") 'tom/shift-right)
(use-package transient)
(use-package xclip
:init (xclip-mode))
(use-package which-key
:commands which-key-mode
:init (which-key-mode))
;; useful functions
(defun tom/unfill-paragraph (&optional region)
"Take REGION and turn it into a single line of text."
(interactive (progn (barf-if-buffer-read-only) '(t)))
(let ((fill-column (point-max))
(emacs-lisp-docstring-fill-column t))
(fill-paragraph nil region)))
(define-key global-map "\M-Q" 'tom/unfill-paragraph)
(defun tom/increment-number-decimal (&optional arg)
"Increment the number forward from point by 'arg'."
(interactive "p*")
(save-excursion
(save-match-data
(let (inc-by field-width answer)
(setq inc-by (if arg arg 1))
(skip-chars-backward "0123456789")
(when (re-search-forward "[0-9]+" nil t)
(setq field-width (- (match-end 0) (match-beginning 0)))
(setq answer (+ (string-to-number (match-string 0) 10) inc-by))
(when (< answer 0)
(setq answer (+ (expt 10 field-width) answer)))
(replace-match (format (concat "%0" (int-to-string field-width) "d")
answer)))))))
(global-set-key (kbd "C-c x") 'tom/increment-number-decimal)
;; other global configurations
;; show current function in modeline
(which-function-mode)
;; scroll screen
(define-key global-map "\M-n" 'end-of-buffer)
(define-key global-map "\M-p" 'beginning-of-buffer)
;; misc iOS fixes
(defun insert-backslash ()
(interactive)
(insert "\\"))
(defun insert-brace-open ()
(interactive)
(insert "{"))
(defun insert-brace-close ()
(interactive)
(insert "}"))
(defun insert-bracket-open ()
(interactive)
(insert "["))
(defun insert-bracket-close ()
(interactive)
(insert "]"))
(defun insert-et-sign ()
(interactive)
(insert "@"))
(defun insert-dollar-sign ()
(interactive)
(insert "$"))
(global-set-key (kbd "M-+") 'insert-backslash)
(global-set-key (kbd "M-2") 'insert-et-sign)
(global-set-key (kbd "M-4") 'insert-dollar-sign)
(global-set-key (kbd "M-7") 'insert-brace-open)
(global-set-key (kbd "M-8") 'insert-bracket-open)
(global-set-key (kbd "M-9") 'insert-bracket-close)
(global-set-key (kbd "M-0") 'insert-brace-close)
(global-set-key (kbd "C-x C-x") 'set-mark-command)
;; change yes/no to y/n
(defalias 'yes-or-no-p 'y-or-n-p)
(setq confirm-kill-emacs 'yes-or-no-p)
;; enable winner-mode, previous window config with C-left
(winner-mode 1)
;; windmove
(windmove-default-keybindings)
;; fonts
(set-face-attribute 'default nil :font "Liberation Mono-14")
(set-face-attribute 'line-number nil :font "Liberation Mono-14")
;; disable tool and menu bars
(tool-bar-mode -1)
(menu-bar-mode -1)
(scroll-bar-mode -1)
(blink-cursor-mode -1)
;; change gc behavior
(setq gc-cons-threshold 50000000)
;; warn when opening large file
(setq large-file-warning-threshold 100000000)
;; disable startup screen
(setq inhibit-startup-screen t)
;; useful frame title format
(setq frame-title-format
'((:eval (if (buffer-file-name)
(abbreviate-file-name (buffer-file-name))
"%b"))))
;; automatic revert
(global-auto-revert-mode t)
;; highlight parenthesis, easier jumping with C-M-n/p
(show-paren-mode 1)
(setq show-paren-style 'expression)
(setq show-paren-delay 0)
;; control indentation
(setq-default indent-tabs-mode nil)
(setq tab-width 4)
(setq c-basic-offset 4)
;; modify scroll settings
(setq scroll-preserve-screen-position t)
;; set default fill width (e.g. M-q)
(setq-default fill-column 79)
;; window dividers
(fringe-mode 0)
(setq window-divider-default-places t
window-divider-default-bottom-width 1
window-divider-default-right-width 1)
(window-divider-mode 1)
;; display time in modeline
(display-time-mode 1)
;; put all backups to same directory to not clutter directories
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
;; display line numbers
(global-display-line-numbers-mode)
;; browse in chrome
(setq browse-url-browser-function 'browse-url-chrome)
(setq shr-width 80)
;; don't fontify latex
(setq font-latex-fontify-script nil)
;; set default encodings to utf-8
(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-language-environment 'utf-8)
(set-selection-coding-system 'utf-8)
;; make Customize to not modify this file
(setq custom-file (make-temp-file "emacs-custom"))
;; enable all disabled commands
(setq disabled-command-function nil)
;; ediff setup
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
;; unbind keys
(unbind-key "C-z" global-map)
;; change emacs frame by number
(defun tom/select-frame (n)
"Select frame identified by the number N."
(interactive)
(let ((frame (nth n (reverse (frame-list)))))
(if frame
(select-frame-set-input-focus frame)
(select-frame-set-input-focus (make-frame)))))
(define-key global-map
(kbd "<f1>")
(lambda () (interactive)
(tom/select-frame 0)))
(define-key global-map
(kbd "<f2>")
(lambda () (interactive)
(tom/select-frame 1)))
(define-key global-map
(kbd "<f3>")
(lambda () (interactive)
(tom/select-frame 2)))
(define-key global-map
(kbd "<f4>")
(lambda () (interactive)
(tom/select-frame 3)))
;; bind find config
(define-key global-map (kbd "<home>")
(lambda () (interactive)
(find-file "~/src/nixemacs/readme.org")))
;; bind compile
(define-key global-map (kbd "<f12>") 'compile)
;; load private configurations
(load "~/Dropbox/Config/emacs/private.el" t)