45 KiB
Luj's literate Emacs configuration
- Early initialisation
- General quality of life improvements
- Theming and user interface
- Editing facilities
- Completions
- Buffer and window management
- My writing tools
- Productivity and agenda
- Mails
- Programming
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 Protesilaos Stavrou's emacs configuration.
(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))
We can then apply the macro to make frame adjustments only in desktop environments.
(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)))
I also adjust the frame color to use the same background as Catpuccin mocha, which is my theme of choice.
(set-face-attribute 'default nil :background "#1e1e2e" :foreground "#ffffff")
Let's setup also additional frame settings that are nice to have.
(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)
Package manager bootstrap
I am using the elpaca
package manager, that needs to be bootstrapped because it is not included in emacs
.
(setq elpaca-core-date '(20250301))
(defvar elpaca-installer-version 0.10)
(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)
(load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
(setq package-enable-at-startup nil)
We now integrate elpaca
with use-package
:
(elpaca elpaca-use-package
(elpaca-use-package-mode))
General quality of life improvements
Remove littering
Prevent emacs
from storing garbage everywhere in my repos.
(use-package no-littering
:ensure t
:demand t
:config
(no-littering-theme-backups))
Save history and editing position
(use-package savehist
:init
(savehist-mode))
(save-place-mode 1)
Accept tangling this file as a hook in a local variable
(add-to-list 'safe-local-eval-forms
'(add-hook 'after-save-hook 'org-babel-tangle nil t))
No impure customizations
(setq custom-file (make-temp-file "emacs-custom-"))
Auto-reverting buffers
(use-package autorevert
:ensure nil
:hook (after-init . global-auto-revert-mode)
:config
(setq auto-revert-verbose t))
Which key
We sometimes need a little help to figure out keybindings, using which-key
for this.
(use-package which-key
:ensure t
:config
(which-key-mode))
Misc changes
(use-package emacs
:ensure nil
:demand t
:config
(setq help-window-select t)
(setq eval-expression-print-length nil)
(setq kill-do-not-save-duplicates t))
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.
(use-package catppuccin-theme
:ensure t
:config
(load-theme 'catppuccin :no-confirm))
I also configure modus-operandi
as my backup theme when I need a light theme.
(use-package modus-themes
:ensure t)
Solaire mode
Allows to have darker colors for non editing buffers, a bit like an IDE.
(use-package solaire-mode
:ensure t
:config
(solaire-global-mode +1))
Mode line
I use the doom-modeline
, it's nice and clean.
(use-package doom-modeline :ensure t
:init
(doom-modeline-mode 1))
Nerd icons
Nerd icons are a nice addition to the UI, I try to enable them wherever I can.
(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))
Line highlighting
(use-package lin
:ensure (:host github :repo "protesilaos/lin")
:config
(setq global-hl-line-sticky-flag t)
(lin-global-mode)
(global-hl-line-mode))
Fonts
(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 180)
(jumbo
:inherit medium
:default-height 260)
(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))))
Editing facilities
Meow
I use meow
as a modal editing framework. It is less invasive than evil-mode
for example.
(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))
Numbers for region extension sometimes slow down my editing workflow, so I disable it.
(use-package meow
:ensure nil
:config
(setq meow-expand-hint-remove-delay 0)
(setq meow-expand-hint-counts nil))
Avy
Avy
is a nice addition to meow
: it allows to jump to specific visible places in the frame using key chords.
(use-package avy
:ensure t
:bind
("C-c j" . avy-goto-char))
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.
(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))
Orderless
With orderless
we can get fuzzy searches in the minibuffer.
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic))
(completion-category-overrides '((file (styles basic partial-completion)))))
Marginalia
Setup contextual information on minibuffer options.
(use-package marginalia
:ensure t
:init
(marginalia-mode))
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.
(use-package consult
:ensure t
:bind
(("C-c b" . consult-buffer)
("C-x C-b" . consult-buffer)))
In buffer completions
Corfu & Cape
Corfu
is the equivalent of vertico
for in buffer completions, for example LSP suggestions, but not limited to that.
(use-package corfu
:ensure t
:config
(setq corfu-auto t)
:init
(global-corfu-mode))
Cape
is a completion provider for corfu
. I mostly use it to complete filenames, mostly.
(use-package cape
:ensure t
:bind ("M-p" . cape-prefix-map)
:init
(add-hook 'completion-at-point-functions #'cape-file))
Buffer and window management
First I define some keybindings that are helpful in order to make window management more straightforward.
(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)))
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.
(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))
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.
(use-package ace-window
:ensure t
:config
(ace-window-display-mode))
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.
(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))
Org modern
To make org-mode
a bit cleaner.
(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))
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.
(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))
I also frequently take "daily" notes, and denote-journal
is a great integration for that.
(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"))
We also load denote-org
, a package containing a handful of useful functions for denote
notes that happen to be org
files.
(use-package denote-org
:ensure (:wait (:host github :repo "protesilaos/denote-org")))
I have this helper function to bring any new file into my notes directory.
(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)))))
Spell checking
I use jinx
for spell checking.
(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"))
Finding synonyms
(use-package powerthesaurus
:ensure t)
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.
(use-package org-ql
:ensure t)
For some reason, org-ql
is freaking out when there is no org header in a file, so I silence the warnings.
(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))
Agenda configuration
To add new tasks to my agenda, I first use a capture template to generally write the task to todo.org
.
(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")))))
I will then refile the tasks in inbox to any of my agenda files sections.
(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))
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.
(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"))))))))
I have a little integration to remove empty lookbooks from my tasks.
(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))
Whenever I mark a tasks as done, it gets archived in my daily note.
(use-package emacs
:ensure nil
: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 don’t 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-to-list '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))
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.
(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"))))
Mails
I use notmuch
to index and search through my large mail inboxes.
(use-package notmuch
:ensure t
:defer t
:config
(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")))
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.
(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)))
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.
(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)
In order to add some integration with common git
forges to open pull request, etc directly from emacs
, I also install forge
.
(use-package forge
:ensure t
:after magit)
Projects
project.el
provide a lot of goodies to work on specific software projects.
(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"))
Proced
Is the emacs
equivalent to htop
.
(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))
Compilation
The compilation buffer is very useful to launch ephemeral processes.
(use-package ansi-color
:hook (compilation-filter . ansi-color-compilation-filter))
(setq compilation-scroll-output t)
Eat (emulate a terminal)
eat
is a terminal emulator implemented directly in emacs-lisp. Apparently, less susceptible to flickering than the others out there.
(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)))
(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))
Direnv
Direnv
is at the core of my software engineering workflows. I use envrc
over direnv.el
because it has an integration with TRAMP
.
(use-package envrc
:ensure t
:config
(envrc-global-mode 1))
LSP integration with eglot
eglot
is the builtin LSP client inside emacs
. It works well, but I add some customization to make it faster.
(use-package eglot
:ensure nil
:config
(fset #'jsonrpc--log-event #'ignore)
(setq eglot-events-buffer-size 0))
Including using eglot-booster
, that bufferize the LSP output instead of emacs
and parses the json
.
(use-package eglot-booster
:ensure (:host github :repo "jdtsmith/eglot-booster")
:after eglot
:config (eglot-booster-mode))
Programming modes
(use-package prog-mode
:ensure nil
:hook
(prog-mode . visual-line-mode)
(prog-mode . display-line-numbers-mode))
Nix
(use-package nix-mode
:ensure t
:after eglot
:hook
(nix-mode . eglot-ensure)
:config
(add-to-list 'eglot-server-programs '(nix-mode . ("nixd"))))
Python
(use-package python-mode
:ensure t
:after eglot
:hook
(python-mode . eglot-ensure))
Rust
(use-package rust-mode
:ensure t
:after eglot
:hook
(rust-mode . eglot-ensure))
Latex
Integrating LaTeX with AUCTex which is apparently an IDE-like for TeX files.
(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))
(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))
Well, why not use it for PDF? Also integrate with LaTeX etc.
(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)))
Flycheck
(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))
Markdown
(use-package markdown-mode
:mode ("README\\.md\\'" . gfm-mode)
:init (setq markdown-command "multimarkdown"))
Haskell
(use-package haskell-mode
:ensure t
:after eglot
:hook
(haskell-mode . eglot-ensure))
Typst
(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"))