emacs-config/ReadMe.org
2025-07-23 20:55:17 +02:00

1650 lines
49 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+title: Luj's literate Emacs configuration
#+author: Luj (Julien Malka)
#+TOC: headlines
#+macro: latest-export-date (eval (format-time-string "%F %T %z"))
#+macro: word-count (eval (count-words (point-min) (point-max)))
*Last revised and exported on {{{latest-export-date}}} with a word count of {{{word-count}}}.*
This is my literate Emacs configuration. It is written with several objectives in mind:
- Minimalism: I am trying to use as little external code as possible, in order to keep the experience fast;
- Functionality: I want all the features of modern editor, and more.
* Early initialisation
The ~early-init.el~ file is the first file loaded during ~emacs~ initialisation. It can be used to setup frames before ~emacs~ actually start loading external code.
** Setting up frame parameters
I want to configure the frame size if I am in a non-tiling window manager, for this I define the ~with-desktop-session~ macro, inspired by [[https://protesilaos.com/emacs/dotemacs][Protesilaos Stavrou's emacs configuration]].
#+begin_src emacs-lisp :tangle "early-init.el"
;;; -*- lexical-binding: t -*-
(defvar luj/tiling-window-manager-regexp "hyprland\\|none\\+exwm"
"Regular expression to tiling window managers.
See definition of the `with-desktop-session' macro.")
(defmacro with-desktop-session (&rest body)
"Expand BODY if desktop session is not a tiling window manager."
(declare (indent 0))
`(when-let* ((session (getenv "DESKTOP_SESSION"))
((not (string-match-p luj/tiling-window-manager-regexp session))))
,@body))
#+end_src
We can then apply the macro to make frame adjustments only in desktop environments.
#+begin_src emacs-lisp :tangle "early-init.el"
(with-desktop-session
(mapc
(lambda (var)
(add-to-list var '(width . (text-pixels . 2200)))
(add-to-list var '(height . (text-pixels . 1200))))
'(default-frame-alist initial-frame-alist)))
#+end_src
I also adjust the frame color to use the same background as Catpuccin mocha, which is my theme of choice.
#+begin_src emacs-lisp :tangle "early-init.el"
(set-face-attribute 'default nil :background "#1e1e2e" :foreground "#ffffff")
#+end_src
Let's setup also additional frame settings that are nice to have.
#+begin_src emacs-lisp :tangle "early-init.el"
(setq frame-resize-pixelwise t
frame-inhibit-implied-resize t
ring-bell-function 'ignore
use-dialog-box t
use-file-dialog nil
use-short-answers t
inhibit-splash-screen t
inhibit-startup-screen t
inhibit-x-resources t
inhibit-startup-echo-area-message user-login-name
inhibit-startup-buffer-menu t)
(menu-bar-mode -1)
(scroll-bar-mode -1)
(tool-bar-mode -1)
#+end_src
** Package manager bootstrap
I am using the ~elpaca~ package manager, that needs to be bootstrapped because it is not included in ~emacs~.
#+begin_src emacs-lisp :tangle "early-init.el"
(setq elpaca-core-date '(20250621))
(defvar elpaca-installer-version 0.11)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil :depth 1 :inherit ignore
:files (:defaults "elpaca-test.el" (:exclude "extensions"))
:build (:not elpaca--activate-package)))
(let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (<= emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (apply #'call-process `("git" nil ,buffer t "clone"
,@(when-let* ((depth (plist-get order :depth)))
(list (format "--depth=%d" depth) "--no-single-branch"))
,(plist-get order :repo) ,repo))))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(let ((load-source-file-function nil)) (load "./elpaca-autoloads"))))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
(setq package-enable-at-startup nil)
#+end_src
We now integrate ~elpaca~ with ~use-package~:
#+begin_src emacs-lisp :tangle "early-init.el"
(elpaca elpaca-use-package
(elpaca-use-package-mode))
#+end_src
* General quality of life improvements
** Remove littering
Prevent ~emacs~ from storing garbage everywhere in my repos.
#+begin_src emacs-lisp :tangle "init.el"
(use-package no-littering
:ensure t
:demand t
:config
(no-littering-theme-backups))
#+end_src
** Save history and editing position
#+begin_src emacs-lisp :tangle "init.el"
(use-package savehist
:init
(savehist-mode))
(save-place-mode 1)
#+end_src
** Accept tangling this file as a hook in a local variable
#+begin_src emacs-lisp :tangle "init.el"
(add-to-list 'safe-local-eval-forms
'(add-hook 'after-save-hook 'org-babel-tangle nil t))
#+end_src
** No impure customizations
#+begin_src emacs-lisp :tangle "init.el"
(setq custom-file (make-temp-file "emacs-custom-"))
#+end_src
** Auto-reverting buffers
#+begin_src emacs-lisp :tangle init.el
(use-package autorevert
:ensure nil
:hook (after-init . global-auto-revert-mode)
:config
(setq auto-revert-verbose t))
#+end_src
** Which key
We sometimes need a little help to figure out keybindings, using ~which-key~ for this.
#+begin_src emacs-lisp :tangle "init.el"
(use-package which-key
:ensure t
:config
(which-key-mode))
#+end_src
** Misc changes
#+begin_src emacs-lisp :tangle "init.el"
(use-package emacs
:ensure nil
:demand t
:config
(setq help-window-select t)
(setq dired-kill-when-opening-new-dired-buffer t)
(setq eval-expression-print-length nil)
(setq kill-do-not-save-duplicates t))
#+end_src
* Theming and user interface
** Main theme
I am a pretty big fan of the Catpuccin color palette, I use the ~mocha~ variation as my main theme.
#+begin_src emacs-lisp :tangle "init.el"
(use-package catppuccin-theme
:ensure t
:config
(load-theme 'catppuccin :no-confirm))
#+end_src
I also configure ~modus-operandi~ as my backup theme when I need a light theme.
#+begin_src emacs-lisp :tangle "init.el"
(use-package modus-themes
:ensure t)
#+end_src
** Solaire mode
Allows to have darker colors for non editing buffers, a bit like an IDE.
#+begin_src emacs-lisp :tangle "init.el"
(use-package solaire-mode
:ensure t
:config
(solaire-global-mode +1))
#+end_src
** Mode line
I use the ~doom-modeline~, it's nice and clean.
#+begin_src emacs-lisp :tangle "init.el"
(use-package doom-modeline :ensure t
:init
(doom-modeline-mode 1))
#+end_src
** Long lines...
... Are painful
#+begin_src emacs-lisp :tangle "init.el"
(setq-default bidi-paragraph-direction 'left-to-right)
(setq bidi-inhibit-bpa t)
(global-so-long-mode 1)
#+end_src
** Nerd icons
Nerd icons are a nice addition to the UI, I try to enable them wherever I can.
#+begin_src emacs-lisp :tangle "init.el"
(use-package nerd-icons-dired
:ensure t
:hook
(dired-mode . nerd-icons-dired-mode))
(use-package nerd-icons-completion
:ensure t
:after marginalia
:config
(nerd-icons-completion-mode)
(add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))
(use-package nerd-icons-corfu
:ensure t
:after corfu
:config
(add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))
#+end_src
** Line highlighting
#+begin_src emacs-lisp :tangle "init.el"
(use-package lin
:ensure (:host github :repo "protesilaos/lin")
:config
(setq global-hl-line-sticky-flag t)
(lin-global-mode)
(global-hl-line-mode))
#+end_src
** Fonts
#+begin_src emacs-lisp :tangle "init.el"
(use-package fontaine
:ensure t
:hook
((after-init . fontaine-mode)
(after-init . (lambda ()
;; Set last preset or fall back to desired style from `fontaine-presets'.
(fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular)))))
:bind (("C-c f" . fontaine-set-preset)
("C-c F" . fontaine-toggle-preset))
:config
(setq x-underline-at-descent-line nil)
(setq-default text-scale-remap-header-line t)
(setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld"))
(setq fontaine-presets
'((small
:default-height 80)
(regular) ; like this it uses all the fallback values and is named `regular'
(medium
:default-family "FiraCode Nerd Font"
:default-height 115
:fixed-pitch-family "FiraCode Nerd Font"
:variable-pitch-family "FiraCode Nerd Font")
(large
:inherit medium
:default-height 150)
(presentation
:inherit medium
:default-height 165)
(jumbo
:inherit medium
:default-height 260)
(dell-docked
:inherit medium
:default-height 200)
(t
:default-weight regular
:default-slant normal
:default-width normal
:default-height 100
:fixed-pitch-weight nil
:fixed-pitch-slant nil
:fixed-pitch-width nil
:fixed-pitch-height 1.0
:fixed-pitch-serif-family nil
:fixed-pitch-serif-weight nil
:fixed-pitch-serif-slant nil
:fixed-pitch-serif-width nil
:fixed-pitch-serif-height 1.0
:variable-pitch-weight nil
:variable-pitch-slant nil
:variable-pitch-width nil
:variable-pitch-height 1.0
:mode-line-active-family nil
:mode-line-active-weight nil
:mode-line-active-slant nil
:mode-line-active-width nil
:mode-line-active-height 1.0
:mode-line-inactive-family nil
:mode-line-inactive-weight nil
:mode-line-inactive-slant nil
:mode-line-inactive-width nil
:mode-line-inactive-height 1.0
:header-line-family nil
:header-line-weight nil
:header-line-slant nil
:header-line-width nil
:header-line-height 1.0
:line-number-family nil
:line-number-weight nil
:line-number-slant nil
:line-number-width nil
:line-number-height 1.0
:tab-bar-family nil
:tab-bar-weight nil
:tab-bar-slant nil
:tab-bar-width nil
:tab-bar-height 1.0
:tab-line-family nil
:tab-line-weight nil
:tab-line-slant nil
:tab-line-width nil
:tab-line-height 1.0
:bold-family nil
:bold-slant nil
:bold-weight bold
:bold-width nil
:bold-height 1.0
:italic-family nil
:italic-weight nil
:italic-slant italic
:italic-width nil
:italic-height 1.0
:line-spacing nil))))
#+end_src
* Editing facilities
** Meow
I use ~meow~ as a modal editing framework. It is less invasive than ~evil-mode~ for example.
#+begin_src emacs-lisp :tangle "init.el"
(defun meow-setup ()
(meow-motion-overwrite-define-key
'("s-SPC" . meow-keypad)
'("j" . meow-next)
'("k" . meow-prev)
'("<escape>" . ignore))
(meow-leader-define-key
'("j" . "H-j")
'("k" . "H-k")
'("1" . meow-digit-argument)
'("2" . meow-digit-argument)
'("3" . meow-digit-argument)
'("4" . meow-digit-argument)
'("5" . meow-digit-argument)
'("6" . meow-digit-argument)
'("7" . meow-digit-argument)
'("8" . meow-digit-argument)
'("9" . meow-digit-argument)
'("0" . meow-digit-argument)
'("/" . meow-keypad-describe-key)
'("?" . meow-cheatsheet))
(meow-normal-define-key
'("s-SPC" . meow-keypad)
'("0" . meow-expand-0)
'("9" . meow-expand-9)
'("8" . meow-expand-8)
'("7" . meow-expand-7)
'("6" . meow-expand-6)
'("5" . meow-expand-5)
'("4" . meow-expand-4)
'("3" . meow-expand-3)
'("2" . meow-expand-2)
'("1" . meow-expand-1)
'("-" . negative-argument)
'(";" . meow-reverse)
'("," . meow-inner-of-thing)
'("." . meow-bounds-of-thing)
'("[" . meow-beginning-of-thing)
'("]" . meow-end-of-thing)
'("a" . meow-append)
'("A" . meow-open-below)
'("b" . meow-back-word)
'("B" . meow-back-symbol)
'("c" . meow-change)
'("d" . meow-delete)
'("D" . meow-backward-delete)
'("e" . meow-next-word)
'("E" . meow-next-symbol)
'("f" . meow-find)
'("g" . meow-cancel-selection)
'("G" . meow-grab)
'("h" . meow-left)
'("H" . meow-left-expand)
'("i" . meow-insert)
'("I" . meow-open-above)
'("j" . meow-next)
'("J" . meow-next-expand)
'("k" . meow-prev)
'("K" . meow-prev-expand)
'("l" . meow-right)
'("L" . meow-right-expand)
'("m" . meow-join)
'("n" . meow-search)
'("o" . meow-block)
'("O" . meow-to-block)
'("p" . meow-yank)
'("q" . meow-quit)
'("Q" . meow-goto-line)
'("r" . meow-replace)
'("R" . meow-swap-grab)
'("s" . meow-kill)
'("t" . meow-till)
'("u" . meow-undo)
'("U" . meow-undo-in-selection)
'("v" . meow-visit)
'("w" . meow-mark-word)
'("W" . meow-mark-symbol)
'("x" . meow-visual-line)
'("X" . meow-goto-line)
'("y" . meow-save)
'("Y" . meow-sync-grab)
'("z" . meow-pop-selection)
'("'" . repeat)
'("<escape>" . ignore)))
(use-package meow
:ensure (:wait t)
:demand t
:custom
(meow-keypad-leader-dispatch "C-c")
:config
(meow-global-mode 1)
(meow-setup))
#+end_src
Numbers for region extension sometimes slow down my editing workflow, so I disable it.
#+begin_src emacs-lisp :tangle "init.el"
(use-package meow
:ensure nil
:config
(setq meow-expand-hint-remove-delay 0)
(setq meow-expand-hint-counts nil))
#+end_src
** Avy
~Avy~ is a nice addition to ~meow~: it allows to jump to specific visible places in the frame using key chords.
#+begin_src emacs-lisp :tangle "init.el"
(use-package avy
:ensure t
:bind
("C-c j" . avy-goto-char))
#+end_src
* Completions
Completions are at the core of the editing experience, I try to use a stack that is both feature-full and yet minimal and well integrated with ~emacs~.
** Minibuffer completions
*** Vertico
~Vertico~ enables VERTIcal COmpletions in the minibuffer.
#+begin_src emacs-lisp :tangle "init.el"
(use-package vertico
:ensure (:files (:defaults "extensions/vertico-directory.el"))
:demand t
:bind (:map vertico-map
("DEL" . vertico-directory-delete-char)
("M-DEL" . vertico-directory-delete-word))
:init
(vertico-mode))
#+end_src
*** Orderless
With ~orderless~ we can get fuzzy searches in the minibuffer.
#+begin_src emacs-lisp :tangle "init.el"
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic))
(completion-category-overrides '((file (styles basic partial-completion)))))
#+end_src
*** Marginalia
Setup contextual information on minibuffer options.
#+begin_src emacs-lisp :tangle "init.el"
(use-package marginalia
:ensure t
:init
(marginalia-mode))
#+end_src
*** Consult
~Consult~ is another nice improvement to minibuffers, where it allows you to live-preview the buffers that would be opened by selecting a given option.
#+begin_src emacs-lisp :tangle "init.el"
(use-package consult
:ensure t
:bind
(("C-c b" . consult-buffer)
("C-x C-b" . consult-buffer)))
#+end_src
** In buffer completions
*** Corfu & Cape
~Corfu~ is the equivalent of ~vertico~ for in buffer completions, for example LSP suggestions, but not limited to that.
#+begin_src emacs-lisp :tangle "init.el"
(use-package corfu
:ensure t
:config
(setq corfu-auto t)
:init
(global-corfu-mode)
(corfu-popupinfo-mode)
(corfu-history-mode)
:config
(setq corfu-popupinfo-delay '(0.2 . 0.1))
(setq corfu-popupinfo-hide nil))
#+end_src
~Cape~ is a completion provider for ~corfu~. I mostly use it to complete filenames, mostly.
#+begin_src emacs-lisp :tangle "init.el"
(use-package cape
:ensure t
:bind ("M-p" . cape-prefix-map)
:init
(add-hook 'completion-at-point-functions #'cape-file))
#+end_src
* Buffer and window management
First I define some keybindings that are helpful in order to make window management more straightforward.
#+begin_src emacs-lisp :tangle "init.el"
(use-package luj-windows
:ensure nil
:after ace-window
:bind
(:map mode-specific-map
("w r" . split-window-right)
("w d" . split-window-below)
("w <right>" . windmove-right)
("w <left>" . windmove-left)
("w <up>" . windmove-up)
("w <down>" . windmove-down)
("w x" . delete-window)
("w w" . other-window)
("w o" . ace-window)))
#+end_src
** Embark
~Embark~ is of course much more versatile than just window/buffer management, but I particularly use it to manage buffer placement when opening them, so... it counts.
#+begin_src emacs-lisp :tangle "init.el"
(use-package embark
:ensure t
:bind
(("C-." . embark-act) ;; pick some comfortable binding
("C-;" . embark-dwim) ;; good alternative: M-.
("C-h B" . embark-bindings)) ;; alternative for `describe-bindings'
:init
;; Optionally replace the key help with a completing-read interface
(setq prefix-help-command #'embark-prefix-help-command)
:config
;; Hide the mode line of the Embark live/completions buffers
(add-to-list 'display-buffer-alist
'("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
nil
(window-parameters (mode-line-format . none)))))
;; Consult users will also want the embark-consult package.
(use-package embark-consult
:ensure t ; only need to install it, embark loads it after consult if found
:hook
(embark-collect-mode . consult-preview-at-point-mode))
#+end_src
** Ace window
In the same spirit as ~avy~, ~ace~ is super useful to select/act on windows when you have a lot of them open.
#+begin_src emacs-lisp :tangle "init.el"
(use-package ace-window
:ensure t
:config
(ace-window-display-mode))
#+end_src
* My writing tools
** Org mode
Obviously, the bulk of my writing activity in ~emacs~ is done in the amazing ~org-mode~. To stay on top of new releases, I use the upstream version.
#+begin_src emacs-lisp :tangle "init.el"
(use-package org
:ensure (:wait t)
:config
;; Exclude the daily tag from inheritance so that archived tasks don't appear with this tag in my agenda
(add-to-list 'org-tags-exclude-from-inheritance "daily")
(setq org-hide-leading-stars t)
(setq org-hide-emphasis-markers t))
#+end_src
** Org modern
To make ~org-mode~ a bit cleaner.
#+begin_src emacs-lisp :tangle "init.el"
(use-package org-modern
:ensure t
:init
(global-org-modern-mode)
:custom
(org-modern-star nil)
(org-modern-block-fringe nil)
(add-hook 'org-agenda-finalize-hook #'org-modern-agenda)
:hook
(org-mode . org-indent-mode)
(org-mode . visual-line-mode))
#+end_src
** Taking notes with Denote
I used to rely on ~org-roam~ for my notetaking, but I like the simpler philosophy of ~denote~, relying on simpler basis and allowing to link to non-org resources.
#+begin_src emacs-lisp :tangle "init.el"
(use-package denote
:ensure (:host github :repo "protesilaos/denote")
:hook (dired-mode . denote-dired-mode)
:bind
(("C-c n n" . denote)
("C-c n r" . denote-rename-file)
("C-c n l" . denote-link)
("C-c n f" . denote-open-or-create)
("C-c n b" . denote-backlinks)
("C-c n d" . denote-sort-dired))
:config
(setq denote-directory
(expand-file-name "~/dev/notes/"))
(setq denote-save-buffers t)
(setq denote-kill-buffers t)
(denote-rename-buffer-mode 1))
#+end_src
I also frequently take "daily" notes, and ~denote-journal~ is a great integration for that.
#+begin_src emacs-lisp :tangle "init.el"
(use-package denote-journal
:ensure (:wait t)
(:host github :repo "protesilaos/denote-journal")
:config
(setq denote-journal-title-format 'day-date-month-year)
(setq denote-journal-directory
(expand-file-name "dailies" denote-directory))
(setq denote-journal-keyword "daily"))
#+end_src
We also load ~denote-org~, a package containing a handful of useful functions for ~denote~ notes that happen to be ~org~ files.
#+begin_src emacs-lisp :tangle "init.el"
(use-package denote-org
:ensure (:wait (:host github :repo "protesilaos/denote-org")))
#+end_src
I have this helper function to bring any new file into my notes directory.
#+begin_src emacs-lisp :tangle "init.el"
(defun denote-add-to-vault ()
(interactive)
(let* ((current-buffer (current-buffer))
(original-content (buffer-string))
(original-filename (buffer-file-name))
(vault-directory (expand-file-name "vault" denote-directory)))
(save-buffer)
(call-interactively #'denote-rename-file)
(let ((new-filename (buffer-file-name)))
(copy-file new-filename
(expand-file-name (file-name-nondirectory new-filename)
vault-directory)
t)
(with-current-buffer current-buffer
(erase-buffer)
(insert original-content)
(save-buffer)))))
#+end_src
** Spell checking
I use ~jinx~ for spell checking.
#+begin_src emacs-lisp :tangle "init.el"
(use-package jinx
:bind (("M-$" . jinx-correct)
("C-M-$" . jinx-languages))
:hook
(text-mode . jinx-mode)
:init
;; Fix warnings with corfu, see <https://github.com/minad/corfu/discussions/457>
(customize-set-variable 'text-mode-ispell-word-completion nil)
(setq text-mode-ispell-word-completion nil)
:config
(add-to-list 'jinx-exclude-faces '(tex-mode font-lock-constant-face))
(setq jinx-languages "en_US fr_FR"))
#+end_src
** Finding synonyms
#+begin_src emacs-lisp :tangle "init.el"
(use-package powerthesaurus
:ensure t)
#+end_src
* Productivity and agenda
** Org-ql
First of all, I use ~org-ql~ to make my agenda faster, and also express custom agenda views more easily.
#+begin_src emacs-lisp :tangle "init.el"
(use-package org-ql
:ensure t)
#+end_src
For some reason, ~org-ql~ is freaking out when there is no org header in a file, so I silence the warnings.
#+begin_src emacs-lisp :tangle "init.el"
(use-package org-ql
:after org-ql
:ensure nil
:config
(defun my-org-ql--select-no-logging-advice (orig-fun &rest args)
"Advice to prevent org-ql--select from logging."
(let ((inhibit-message t)
(message-log-max nil))
(apply orig-fun args)))
(advice-add 'org-ql--select :around #'my-org-ql--select-no-logging-advice))
#+end_src
** Agenda configuration
To add new tasks to my agenda, I first use a capture template to generally write the task to ~todo.org~.
#+begin_src emacs-lisp :tangle "init.el"
(use-package emacs
:ensure nil
:demand t
:after denote-org
:config
(global-set-key "\C-ca" 'org-agenda)
(setq org-agenda-files
(append '("~/dev/todos/inbox.org"
"~/dev/todos/these.org"
"~/dev/todos/freelance.org"
"~/dev/todos/perso.org"
"~/dev/todos/oss.org")
(list (denote-journal-path-to-new-or-existing-entry))))
(setq org-todo-keywords
(quote ((sequence "TODO(t)" "|" "DONE(d)")
(sequence "WAITING(w@/!)" "HOLD(h@/!)" "|" "CANCELLED(c@/!)" "PHONE" "MEETING"))))
(setq org-use-fast-todo-selection t)
(setq org-treat-S-cursor-todo-selection-as-state-change nil)
(global-set-key (kbd "C-c c") 'org-capture)
(setq org-capture-templates
(quote (("t" "todo" entry (file "~/dev/todos/inbox.org")
"* TODO %?\n\n%a\n" :clock-in t :clock-resume t)
("w" "org-protocol" entry (file "~/dev/todos/inbox.org")
"* TODO Review %c\n%U\n" :immediate-finish t)
("m" "Meeting" entry (file "~/dev/todos/inbox.org")
"* MEETING with %? :MEETING:\n%U" :clock-in t :clock-resume t)
("p" "Phone call" entry (file "~/dev/todos/inbox.org")
"* PHONE %? :PHONE:\n%U" :clock-in t :clock-resume t)
("h" "Habit" entry (file "~/dev/todos/inbox.org")
"* NEXT %?\n%U\n%a\nSCHEDULED: %(format-time-string \"%<<%Y-%m-%d %a .+1d/3d>>\")\n:PROPERTIES:\n:STYLE: habit\n:REPEAT_TO_STATE: NEXT\n:END:\n")))))
(advice-add 'org-agenda-quit :before 'org-save-all-org-buffers)
#+end_src
I will then refile the tasks in inbox to any of my agenda files sections.
#+begin_src emacs-lisp :tangle "init.el"
(use-package emacs
:ensure nil
:config
(setq org-refile-targets (quote ((nil :maxlevel . 9)
(org-agenda-files :maxlevel . 9))))
(setq org-refile-use-outline-path t)
(setq org-outline-path-complete-in-steps nil)
(setq org-refile-allow-creating-parent-nodes (quote confirm))
(defun luj/verify-refile-target ()
"Exclude todo keywords with a done state from refile targets"
(not (member (nth 2 (org-heading-components)) org-done-keywords)))
(setq org-refile-target-verify-function 'luj/verify-refile-target))
#+end_src
I have two main agenda views:
- One that shows all the tasks in my agenda in a TODO state, that I use to schedule tasks that I should do on a given day;
- One that is a specialized version that shows only what is scheduled and has been done on the given day.
#+begin_src emacs-lisp :tangle "init.el"
(use-package emacs
:ensure nil
:config
(setq org-agenda-custom-commands
'(("p" "Planner"
((org-ql-block '(and
(todo "TODO")
(tags "inbox"))
((org-ql-block-header "Inbox")))
(org-ql-block '(and
(not (tags "inbox"))
(not (todo "DONE"))
(scheduled :to today))
((org-ql-block-header "Already scheduled today")))
(org-ql-block '(or
(and
(not (tags "inbox"))
(descendants (and (not (scheduled :to today))(todo "TODO")))
(not (scheduled :to today)))
(and
(not (tags "inbox"))
(ancestors)
(todo "TODO")
(not (scheduled :to today))))
((org-ql-block-header "Tasks to pick")))))
("t" "Today tasks"
((org-ql-block '(or
(and
(not (tags "inbox"))
(descendants (and (scheduled :to today)(todo "TODO"))))
(and
(not (tags "inbox"))
(descendants (and (closed :to today)(todo "DONE"))))
(and
(not (tags "inbox"))
(ancestors)
(todo "TODO")
(scheduled :to today))
(and
(not (tags "inbox"))
(ancestors)
(todo "DONE")
(closed :to today)))
((org-ql-block-header "Today tasks"))))))))
#+end_src
I have a little integration to remove empty lookbooks from my tasks.
#+begin_src emacs-lisp :tangle "init.el"
(use-package emacs
:ensure nil
:demand t
:config
;; Remove empty clocks
(setq org-clock-out-remove-zero-time-clocks t)
;; And remove empty logbooks
(defun luj/remove-empty-drawer-on-clock-out ()
(interactive)
(save-excursion
(beginning-of-line 0)
(org-remove-empty-drawer-at (point))))
(add-hook 'org-clock-out-hook 'luj/remove-empty-drawer-on-clock-out 'append))
#+end_src
Whenever I mark a tasks as done, it gets archived in my daily note.
#+begin_src emacs-lisp :tangle "init.el"
(use-package org-archive
:ensure nil
:after denote-org
:after org
:config
(setq org-archive-file-header-format nil)
(setq org-archive-default-command #'org-archive-subtree-hierarchically)
(defun org-archive-subtree-hierarchically (&optional prefix)
(interactive "P")
(let* ((fix-archive-p (and (not prefix)
(not (use-region-p))))
(afile (car (org-archive--compute-location
(or (org-entry-get nil "ARCHIVE" 'inherit) org-archive-location))))
(buffer (or (find-buffer-visiting afile) (find-file-noselect afile))))
(org-archive-subtree prefix)
(when fix-archive-p
(with-current-buffer buffer
(goto-char (point-max))
(while (org-up-heading-safe))
(let* ((olpath (org-entry-get (point) "ARCHIVE_OLPATH"))
(path (and olpath (split-string olpath "/")))
(level 1)
tree-text)
(when olpath
(org-mark-subtree)
(setq tree-text (buffer-substring (region-beginning) (region-end)))
;; we dont want to see "Cut subtree" messages
(let (this-command (inhibit-message t)) (org-cut-subtree))
(goto-char (point-min))
(save-restriction
(widen)
(-each path
(lambda (heading)
(if (re-search-forward
(rx-to-string
`(: bol (repeat ,level "*") (1+ " ") ,heading)) nil t)
(org-narrow-to-subtree)
(goto-char (point-max))
(unless (looking-at "^")
(insert "\n"))
(insert (make-string level ?*)
" "
heading
"\n"))
(cl-incf level)))
(widen)
(org-end-of-subtree t t)
(org-paste-subtree level tree-text))))))))
(defun luj/archive-to-location (location)
(interactive)
(setq org-archive-location location)
(setq org-archive-subtree-add-inherited-tags t)
(org-archive-subtree-hierarchically))
(defun luj/org-roam-archive-todo-to-today ()
(interactive)
(let ((org-archive-hook #'save-buffer)
today-file
pos)
(save-window-excursion
(denote-journal-new-or-existing-entry)
(setq today-file (buffer-file-name))
(setq pos (point)))
;; Only archive if the target file is different than the current file
(unless (equal (file-truename today-file)
(file-truename (buffer-file-name)))
(luj/archive-to-location (concat today-file "::* Tasks done")))))
(add-hook 'org-after-todo-state-change-hook
(lambda ()
(when (member org-state '("DONE" "CANCELLED" "READ"))
(luj/org-roam-archive-todo-to-today))))
(setq org-log-done 'time))
#+end_src
** Weekly reports
I often want to generate weekly reports from the tasks I did since the last report. For this I parse all the archived notes in the dailies, extract all the tasks and filter them if they have been done for my PhD.
#+begin_src emacs-lisp :tangle "init.el"
(defun luj/file-greater-than-weekly-p (weekly-file file)
(let* ((date-weekly (denote-retrieve-filename-identifier weekly-file))
(date-file (denote-retrieve-filename-identifier file)))
(apply '<= (mapcar (lambda (x) (time-to-seconds (date-to-time x))) (list date-weekly date-file)))))
(defun luj/dailies-greater-than-weekly (weekly-file files)
(let ((partial (apply-partially 'luj/file-greater-than-weekly-p weekly-file)))
(seq-filter partial files)))
(defun luj/get-latest-weekly (&optional directory)
(let* ((weekly-dir (expand-file-name "weeklies" denote-directory))
(files (directory-files (or directory weekly-dir))))
(car (last files))))
(defun luj/remove-drawers-task (task)
(org-element-map task '(drawer property-drawer planning)
(lambda (d) (org-element-extract-element d)))
task)
(defun luj/extract-tasks-file (file-path)
(let*
((tree (with-temp-buffer
(insert-file-contents file-path)
(org-mode)
(org-element-parse-buffer)))
(tasks (org-element-map tree 'headline
(lambda (elem)
(when (and
(string= "DONE" (org-element-property :todo-keyword elem))
(string= "these" (org-element-property :ARCHIVE_CATEGORY elem)))
elem)))))
(mapcar 'luj/remove-drawers-task tasks)))
(defun luj/next-tuesday ()
(let ((human-time "next Tuesday"))
(parse-time-string (with-temp-buffer
(call-process "env" nil t nil "LC_ALL=C" "LANGUAGE=" "date" "-d" human-time)
(or (bobp) (delete-backward-char 1))
(buffer-string)))))
(defun generate-weekly-report ()
(interactive)
(let* ((latest-weekly (luj/get-latest-weekly))
(daily-dir (expand-file-name "dailies" denote-directory))
(daily-files (directory-files daily-dir :match ".org"))
(dailies-since-weekly (luj/dailies-greater-than-weekly latest-weekly daily-files))
(all-tasks (apply 'append
(mapcar 'luj/extract-tasks-file dailies-since-weekly)))
(next-weekly-date (format-time-string "%Y-%m-%d" (encode-time (luj/next-tuesday))))
(title (concat next-weekly-date " - Weekly report"))
(weekly-dir (expand-file-name "weeklies" denote-directory))
(weekly-file (denote title '("weekly") 'org weekly-dir next-weekly-date))
)
(find-file weekly-file)
(org-mode)
(let* ((tree (org-element-parse-buffer))
(top-heading (org-element-create 'headline
(list :title "Report"
:level 1))))
(dolist (task all-tasks)
(org-element-put-property task :level 2)
(org-element-adopt-elements top-heading task))
(org-element-map tree 'org-data
(lambda (document)
(org-element-adopt-elements document top-heading)))
(erase-buffer)
(insert (org-element-interpret-data tree))
(write-file weekly-file)
(message "Weekly report created"))))
#+end_src
* Mails
I use ~notmuch~ to index and search through my large mail inboxes.
#+begin_src emacs-lisp :tangle "init.el"
(use-package notmuch
:ensure t
:init
(setq notmuch-search-oldest-first nil)
(setq notmuch-show-logo nil
notmuch-column-control 1.0
notmuch-hello-auto-refresh t
notmuch-hello-recent-searches-max 20
notmuch-hello-thousands-separator ""
notmuch-hello-sections '(notmuch-hello-insert-saved-searches)
notmuch-show-all-tags-list t)
(setq notmuch-show-empty-saved-searches t)
(setq notmuch-saved-searches
`(( :name "telecom/inbox"
:query "tag:inbox and tag:telecom and folder:telecom/INBOX"
:sort-order newest-first
:key ,(kbd "i"))
( :name "telecom/CGT"
:query "tag:telecom and tag:inbox and tag:cgt"
:sort-order newest-first
:key ,(kbd "u"))
( :name "telecom/all"
:query "tag:telecom and tag:inbox"
:sort-order newest-first
:key ,(kbd "o"))
( :name "work/inbox"
:query "tag:work and folder:work/INBOX"
:sort-order newest-first
:key ,(kbd "p"))
( :name "work/RB"
:query "tag:work and tag:inbox and tag:reproducible-builds"
:sort-order newest-first
:key ,(kbd "c"))
( :name "work/bluehats"
:query "tag:work and tag:inbox and tag:bluehats"
:sort-order newest-first
:key ,(kbd "p"))
( :name "work/all"
:query "tag:work and tag:inbox"
:sort-order newest-first
:key ,(kbd "p"))))
(setq notmuch-search-result-format
'(("date" . "%12s ")
("count" . "%-7s ")
("authors" . "%-20s ")
("subject" . "%-80s ")
("tags" . "(%s)")))
(setq notmuch-tree-result-format
'(("date" . "%12s ")
("authors" . "%-20s ")
((("tree" . "%s")
("subject" . "%s"))
. " %-80s ")
("tags" . "(%s)")))
(setq notmuch-search-oldest-first nil)
(add-to-list 'meow-mode-state-list '(notmuch-hello-mode . motion))
(add-to-list 'meow-mode-state-list '(notmuch-search-mode . motion))
(add-to-list 'meow-mode-state-list '(notmuch-tree-mode . motion))
(add-to-list 'meow-mode-state-list '(notmuch-show-mode . motion))
(setq mail-specify-envelope-from t)
(setq message-sendmail-envelope-from 'header)
(setq mail-envelope-from 'header))
(use-package notmuch-addr
:ensure t)
(with-eval-after-load 'notmuch-address
(notmuch-addr-setup))
(setq sendmail-program "msmtp"
send-mail-function 'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-envelope-from 'header
message-send-mail-function 'message-send-mail-with-sendmail)
(setq notmuch-fcc-dirs
'(("julien.malka@ens.fr" . "ens/Sent")
("julien@malka.sh" . "work/Sent")
("jmalka@dgnum.eu" . "dgnum/Sent")
("julien.malka@telecom-paris.fr" . "telecom/Sent")))
#+end_src
I want the number of unread messages in my ~notmuch-hello~ buffer.
#+begin_src emacs-lisp :tangle init.el
(defun luj/notmuch-hello-insert-buttons (searches)
"Insert buttons for SEARCHES.
SEARCHES must be a list of plists each of which should contain at
least the properties :name NAME :query QUERY and :count COUNT,
where QUERY is the query to start when the button for the
corresponding entry is activated, and COUNT should be the number
of messages matching the query. Such a plist can be computed
with `notmuch-hello-query-counts'."
(let* ((widest (notmuch-hello-longest-label searches))
(tags-and-width (notmuch-hello-tags-per-line widest))
(tags-per-line (car tags-and-width))
(column-width (cdr tags-and-width))
(column-indent 0)
(count 0)
(reordered-list (notmuch-hello-reflect searches tags-per-line))
;; Hack the display of the buttons used.
(widget-push-button-prefix "")
(widget-push-button-suffix ""))
;; dme: It feels as though there should be a better way to
;; implement this loop than using an incrementing counter.
(mapc (lambda (elem)
;; (not elem) indicates an empty slot in the matrix.
(when elem
(when (> column-indent 0)
(widget-insert (make-string column-indent ? )))
(let* ((name (plist-get elem :name))
(query (plist-get elem :query))
(query-unread (concat query " and tag:unread"))
(count-unread (string-to-number (notmuch-saved-search-count query-unread)))
(oldest-first (cl-case (plist-get elem :sort-order)
(newest-first nil)
(oldest-first t)
(otherwise notmuch-search-oldest-first)))
(exclude (cl-case (plist-get elem :excluded)
(hide t)
(show nil)
(otherwise notmuch-search-hide-excluded)))
(search-type (plist-get elem :search-type))
(msg-count (plist-get elem :count)))
(widget-insert (format "%8s(%2s) "
(notmuch-hello-nice-number msg-count)
(if (>= count-unread 100)
"xx"
(notmuch-hello-nice-number count-unread))))
(widget-create 'push-button
:notify #'notmuch-hello-widget-search
:notmuch-search-terms query
:notmuch-search-oldest-first oldest-first
:notmuch-search-type search-type
:notmuch-search-hide-excluded exclude
name)
(setq column-indent
(1+ (max 0 (- column-width (length name)))))))
(cl-incf count)
(when (eq (% count tags-per-line) 0)
(setq column-indent 0)
(widget-insert "\n")))
reordered-list)
;; If the last line was not full (and hence did not include a
;; carriage return), insert one now.
(unless (eq (% count tags-per-line) 0)
(widget-insert "\n"))))
(advice-add 'notmuch-hello-insert-buttons :override #'luj/notmuch-hello-insert-buttons)
#+end_src
* Programming
** Git adventures
~Emacs~ has an amazing ~git~ client available as the ~magit~ package; it replaces all my ~git~ usage.
~Magit~ depends on a more recent version of ~transient~ than the one my ~emacs~ comes with, so we fetch it.
#+begin_src emacs-lisp :tangle "init.el"
(use-package transient
:ensure t
:defer t
:config
(setq transient-show-popup 0.5))
(use-package magit
:ensure t
:bind
("C-c d" . magit-discard)
("C-c g" . magit-status)
:config
(setq magit-diff-refine-hunk 'all)
(setq git-commit-style-convention-checks '(non-empty-second-line overlong-summary-line))
(with-eval-after-load 'magit
(setq magit-format-file-function #'magit-format-file-nerd-icons)))
#+end_src
I regularly work within ~nixpkgs~, a huge repository that can make ~git~ and consequently ~magit~ slow. Let's fix it by disabling features for this repository.
#+begin_src emacs-lisp :tangle "init.el"
(dir-locals-set-class-variables
'huge-git-repository
'((nil
. ((magit-refresh-buffers . nil)
(magit-revision-insert-related-refs . nil)))
(magit-status-mode
. ((eval . (magit-disable-section-inserter 'magit-insert-tags-header))
(eval . (magit-disable-section-inserter 'magit-insert-recent-commits))
(eval . (magit-disable-section-inserter 'magit-insert-unpushed-to-pushremote))
(eval . (magit-disable-section-inserter 'magit-insert-unpushed-to-upstream-or-recent))
(eval . (magit-disable-section-inserter 'magit-insert-unpulled-from-pushremote))
(eval . (magit-disable-section-inserter 'magit-insert-unpulled-from-upstream))
))))
(dir-locals-set-directory-class
"/home/julien/dev/nixpkgs/" 'huge-git-repository)
#+end_src
In order to add some integration with common ~git~ forges to open pull request, etc directly from ~emacs~, I also install ~forge~.
#+begin_src emacs-lisp :tangle "init.el"
(use-package forge
:ensure t
:after magit)
#+end_src
** Projects
~project.el~ provide a lot of goodies to work on specific software projects.
#+begin_src emacs-lisp :tangle "init.el"
(use-package project
:bind
(("C-c p p" . project-switch-project)
("C-c p f" . project-find-file)
("C-c p b" . consult-project-buffer)
("C-c p c" . project-compile)
("C-c p d" . project-dired))
:custom
(project-vc-merge-submodules nil)
:config
(add-to-list 'project-switch-commands '(project-dired "Dired"))
(project-remember-projects-under "/home/julien/dev"))
#+end_src
** Proced
Is the ~emacs~ equivalent to ~htop~.
#+begin_src emacs-lisp :tangle init.el
(use-package proced
:ensure nil
:commands (proced)
:config
(setq proced-auto-update-flag 'visible)
(setq proced-enable-color-flag t)
(setq proced-auto-update-interval 5)
(setq proced-descend t))
#+end_src
** Compilation
The compilation buffer is very useful to launch ephemeral processes.
#+begin_src emacs-lisp :tangle "init.el"
(use-package ansi-color
:hook (compilation-filter . ansi-color-compilation-filter))
(setq compilation-scroll-output t)
#+end_src
** Eat (emulate a terminal)
~eat~ is a terminal emulator implemented directly in emacs-lisp. Apparently, less susceptible to flickering than the others out there.
#+begin_src emacs-lisp :tangle "init.el"
(use-package eat
:ensure
(:type git
:repo "https://codeberg.org/vifon/emacs-eat.git"
:branch "fish-integration"
:files (:defaults "terminfo"
"integration")
:config
(add-hook 'eshell-load-hook #'eat-eshell-mode)))
(use-package vterm
:ensure t)
(defun eat-meow-setup ()
(add-hook 'meow-normal-mode-hook 'eat-emacs-mode nil t)
(add-hook 'meow-insert-mode-hook
(lambda ()
(goto-char (point-max))
(eat-char-mode))
nil
t))
(with-eval-after-load "eat"
;; Replace semi-char mode with emacs mode
(advice-add 'eat-semi-char-mode :after 'eat-emacs-mode)
(add-hook 'eat-mode-hook 'eat-emacs-mode)
(add-hook 'eat-mode-hook 'eat-meow-setup))
#+end_src
** Direnv
~Direnv~ is at the core of my software engineering workflows. I use ~envrc~ over ~direnv.el~ because it has an integration with ~TRAMP~.
#+begin_src emacs-lisp :tangle "init.el"
(use-package envrc
:ensure t
:config
(envrc-global-mode 1))
#+end_src
** LSP integration with eglot
~eglot~ is the builtin LSP client inside ~emacs~. It works well, but I add some customization to make it faster.
#+begin_src emacs-lisp :tangle "init.el"
(use-package eglot)
#+end_src
Including using ~eglot-booster~, that bufferize the LSP output instead of ~emacs~ and parses the ~json~.
#+begin_src emacs-lisp :tangle "init.el"
(use-package eglot-booster
:ensure (:host github :repo "jdtsmith/eglot-booster")
:after eglot
:config (eglot-booster-mode))
#+end_src
I don't like how I must open another buffer to get documentation, so let's go with ~eldoc-box~
#+begin_src emacs-lisp :tangle "init.el"
(use-package eldoc-box
:ensure t
:config
(eldoc-box-hover-at-point-mode)
(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t)
)
#+end_src
** Programming modes
#+begin_src emacs-lisp :tangle "init.el"
(use-package prog-mode
:ensure nil
:hook
(prog-mode . visual-line-mode)
(prog-mode . display-line-numbers-mode))
#+end_src
*** Nix
#+begin_src emacs-lisp :tangle "init.el"
(use-package nix-mode
:ensure t
:after eglot
:hook
(nix-mode . eglot-ensure)
:config
(add-to-list 'eglot-server-programs '(nix-mode . ("nixd"))))
#+end_src
*** Python
#+begin_src emacs-lisp :tangle "init.el"
(use-package python-mode
:ensure t
:after eglot
:hook
(python-mode . eglot-ensure))
#+end_src
*** Rust
#+begin_src emacs-lisp :tangle "init.el"
(use-package rust-mode
:ensure t
:after eglot
:hook
(rust-mode . eglot-ensure))
#+end_src
*** Latex
Integrating LaTeX with AUCTex which is apparently an IDE-like for TeX files.
#+begin_src emacs-lisp :tangle "init.el"
(use-package auctex
:ensure t
:defer t
:mode ("\\.tex\\'" . latex-mode)
:config
(setq TeX-auto-save t)
(setq TeX-parse-self t)
(setq-default TeX-master nil))
#+end_src
#+begin_src emacs-lisp :tangle "init.el"
(use-package latex-preview-pane
:ensure t
:custom
(latex-preview-pane-multifile-mode 'AucTeX)
:config
(latex-preview-pane-enable)
(setq latex-preview-pane-use-frame t))
#+end_src
*** PDF
Well, why not use it for PDF? Also integrate with /LaTeX/ etc.
#+begin_src emacs-lisp :tangle "init.el"
(use-package pdf-tools
:ensure t
:init (pdf-tools-install)
:config
(setq TeX-view-program-selection '((output-pdf "PDF Tools"))
TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view))
TeX-source-correlate-start-server t)
(add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer)
(setq pdf-view-use-scaling t)
(setq pdf-view-resize-factor 1.05)
:bind (:map pdf-view-mode-map
("<left>" . pdf-view-previous-page-command)
("<right>" . pdf-view-next-page-command)))
#+end_src
*** Flycheck
#+begin_src emacs-lisp :tangle "init.el"
(use-package flycheck
:ensure t
:bind
(("C-c e e" . consult-flycheck)
("C-c e a" . eglot-code-actions))
:init
(global-flycheck-mode))
(use-package flycheck-eglot
:ensure t
:after (flycheck eglot)
:config
(global-flycheck-eglot-mode 1))
(use-package consult-flycheck
:ensure t)
(use-package eglot-ltex
:ensure (:host github :repo "emacs-languagetool/eglot-ltex")
:init
(setq eglot-ltex-server-path "ltex-cli"
eglot-ltex-communication-channel 'stdio))
#+end_src
*** Markdown
#+begin_src emacs-lisp :tangle "init.el"
(use-package markdown-mode
:mode ("README\\.md\\'" . gfm-mode)
:init (setq markdown-command "multimarkdown"))
#+end_src
*** Haskell
#+begin_src emacs-lisp :tangle "init.el"
(use-package haskell-mode
:ensure t
:after eglot
:hook
(haskell-mode . eglot-ensure))
#+end_src
*** Typst
#+begin_src emacs-lisp :tangle "init.el"
(use-package typst-ts-mode
:after eglot
:ensure (:type git
:repo "https://codeberg.org/meow_king/typst-ts-mode.git"
:branch "main")
:hook
(typst-ts-mode . eglot-ensure)
:config
(add-to-list 'eglot-server-programs '(typst-ts-mode . ("tinymist"))))
(use-package websocket
:ensure t)
(use-package typst-preview
:after websocket
:ensure (:host github :repo "havarddj/typst-preview.el"))
#+end_src
*** Lisp
#+begin_src emacs-lisp :tangle "init.el"
(use-package sly
:ensure t)
#+end_src