emacs configuration mirror jeromenerf's Emacs config

This file is sourced by .emacs, babel'd to a lisp file by extruding the code blocks. Use C-c ' to edit a code block within a split window.

I use emacs mostly for org-mode, to write all sorts of documents, but little to no code. I use vim for that.

Priorities for a "prose editor" differ from "code editor":

  1. the output matters as much as the input
  2. reading and writing comfort is a must
  3. calendar integration is important
  4. synchronization cannot rely only on git, to allow mobile use

1 initialize package manager

use-package may already be set in .emacs.

;(require 'package)
;(require 'use-package)
;(setq use-package-always-ensure t)

and make sure to auto update them

(use-package auto-package-update
  :ensure t
  :config
  (setq auto-package-update-delete-old-versions t
        auto-package-update-interval 4)
  (auto-package-update-maybe))

2 generic configuration

Emacs starts in its config folder by default. As we mostly use org mode, let's start in the default org dir.

(setq default-directory (concat (getenv "HOME") "/org" ))

2.1 default look and feel

Remove bars, scrolls, menus, … Make emacs as chrome less as possible.

(setq inhibit-startup-message t)
(menu-bar-mode -1)
(toggle-scroll-bar -1)
(tool-bar-mode -1)
(setq visible-bell 1)
(set-face-attribute 'default nil :font "Go mono 10")
(set-frame-font "Go mono 10" nil t)
(setq vc-follow-symlinks t)
(global-visual-line-mode)
(setq split-height-threshold 100)
(setq split-width-threshold 90)
(column-number-mode)
(line-number-mode)

2.2 server mode

Also, always start a daemon, to be used with emacsclient, just as vim remote.

(server-mode)

2.3 mouse settings

Once again, the defaults are annoying.

(setq mouse-wheel-scroll-amount '(1 ((shift) . 1))) ;; one line at a time
(setq mouse-wheel-progressive-speed t) ;; do accelerate scrolling
(setq mouse-wheel-follow-mouse 't) ;; scroll window under mouse
(setq scroll-step 1) ;; keyboard scroll one line at a time

(global-set-key (kbd "<C-mouse-4>") 'text-scale-increase)
(global-set-key (kbd "<C-mouse-5>") 'text-scale-decrease)
(global-set-key (kbd "<mouse-8>") 'next-buffer)
(global-set-key (kbd "<mouse-9>") 'previous-buffer)

;; smooth scroll
(setq scroll-margin 5
      scroll-conservatively 9999
      scroll-step 1)

;; Can we get some acme like features, such as follow link on right click?

2.4 configure backup files, autosave and auto-reload buffer when file changes

emacs autosaves using #filename# and backup using filename~ in the current directory. Let's move all of them to ~/.emacs/backup to clear up the crap.

Let's start with backup files:

(setq backup-directory-alist
      `(("." . ,(concat user-emacs-directory "backups"))))
(setq backup-directory-alist
      `((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
      `((".*" ,temporary-file-directory t)))

(setq make-backup-files t               ; backup of a file the first time it is saved.
      backup-by-copying t               ; don't clobber symlinks
      version-control t                 ; version numbers for backup files
      delete-old-versions t             ; delete excess backup files silently
      delete-by-moving-to-trash t
      kept-old-versions 6               ; oldest versions to keep when a new numbered backup is made (default: 2)
      kept-new-versions 9               ; newest versions to keep when a new numbered backup is made (default: 2)
      auto-save-default t               ; auto-save every buffer that visits a file
      auto-save-timeout 20              ; number of seconds idle time before auto-save (default: 30)
      auto-save-interval 200            ; number of keystrokes between auto-saves (default: 300)
      )

Then try to auto-save all "visited", buffers with backend files (not the scratch, etc):

(setq auto-save-default t)
(setq auto-save-mode t)
(setq auto-save-visited-mode t)
(add-hook 'focus-out-hook 'do-auto-save)

Then, we also want emacs to revert the buffer if the underlying file as changed. There should not be conflicts if the buffer is written to file quickly. Let's hope so.

(global-auto-revert-mode t)             ; reload the buffer is the file changes underneath
                                        ; (auto-revert-use-notify nil)          ; poll for changes 5s, instead of fs notification

2.5 configure a clean light theme

Leuven is a great theme, especially with org. It's a light theme focusing on readability.

(setq-default frame-title-format "emacs: %f")
(use-package leuven-theme
  :ensure t )
(load-theme 'leuven t) 

Olivetti is a nice focus mode, centered and all, goyo style.

(use-package olivetti
  :ensure t
  )

3 packages

3.1 fuzzy finder

(use-package prescient
  :ensure t
  :config
  (prescient-persist-mode +1)
  )
(use-package selectrum
  :ensure t
  :config
  (setq enable-recursive-minibuffers t)
  (selectrum-mode +1)
  )
(use-package selectrum-prescient
  :ensure t
  :config
  (selectrum-prescient-mode +1)
  )

Emacs fuzzy file finders suck and I barely need them for org files, so let's hack a quick wrapper around rofi and emacsclient, using the tips from https://www.emacswiki.org/emacs/ExecuteExternalCommand

(defun my-org-rofi ()
  "Use rofi and ag to fuzzy find an org file and open it."
  (interactive)
  (start-process "rofi" nil "rofi-org-file-finder.sh"))
cat $HOME/bin/rofi-org-file-finder.sh

3.2 evil: vi mode for emacs

(use-package evil
  :config
  (setq evil-vsplit-window-right t)
  (setq evil-split-window-below t)
  (evil-mode 1)
  (evil-set-leader 'normal (kbd "<SPC>") nil)
  (evil-define-key 'normal 'global (kbd "<leader>b") 'switch-to-buffer)
  (evil-define-key 'normal 'global (kbd "<leader>p") 'my-org-rofi)
  (evil-define-key 'normal 'global (kbd "<leader>x") 'execute-extended-command)
  )

(use-package evil-surround
  :ensure t
  :config
  (global-evil-surround-mode 1))

3.3 ESC to quit / exit / stop everywhere t

;; esc quits
(defun minibuffer-keyboard-quit ()
  "Abort recursive edit.
In Delete Selection mode, if the mark is active, just deactivate it;
then it takes a second \\[keyboard-quit] to abort the minibuffer."
  (interactive)
  (if (and delete-selection-mode transient-mark-mode mark-active)
      (setq deactivate-mark  t)
    (when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
    (abort-recursive-edit)))
(define-key evil-normal-state-map [escape] 'keyboard-quit)
(define-key evil-visual-state-map [escape] 'keyboard-quit)
(define-key minibuffer-local-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-ns-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-completion-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-must-match-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-isearch-map [escape] 'minibuffer-keyboard-quit)
(global-set-key [escape] 'evil-exit-emacs-state)

3.4 Jump from project to project, using "projectile"

This is useful to switch between distant projects, blog, project todo, etc.

projectile-switch-project allows to switch between projects.

(use-package projectile
  :diminish ("Projectile" "")
  :config
  (setq projectile-project-search-path '(
                                         "~/org"
                                         "~/go/src/git.jardinmagique.info/jeromenerf"
                                         "~/go/src/gitlab.com/jeromenerf"
                                         "~/go/src/gitlab.com/scpo2020/"
                                         "~/Documents/"
                                         "~/src"))
  (projectile-global-mode))

3.5 Autocomplete, using company?

Not that useful for me, since I don't write lots of code in emacs. Maybe useful for emacs lisp.

(use-package company
  :diminish company-mode
  :config
  (setq company-minimum-prefix-length 2)
  (setq company-tooltip-limit 20)                      ; bigger popup window
  (setq company-idle-delay .3)                         ; decrease delay before autocompletion popup shows
  (setq company-echo-delay 0)                          ; remove annoying blinking
  (setq company-begin-commands '(self-insert-command)) ; start autocompletion only after typing

  (global-company-mode 1))

3.6 org

<3

3.6.1 org mode defaults settings

(use-package org
  :mode (("\\.org" . org-mode))
  :bind (:map org-mode-map
              ("<C-return>" . org-meta-return)
              ("<C-S-return>" . org-insert-todo-heading)
              ("<C-up>" . org-metaup)
              ("<C-down>" . org-metadown)
              ("<C-right>" . org-metaright)
              ("<C-left>" . org-metaleft)
              ("<C-S-up>" . org-shiftmetaup)
              ("<C-S-down>" . org-shiftmetadown)
              ("<C-S-right>" . org-shiftmetaright)
              ("<C-S-left>" . org-shiftmetaleft)
              )
  :config
  (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
  (setq org-log-into-drawer t)
  (setq org-export-headline-levels 9)
  (setq org-todo-keywords '((sequence "TODO" "DOING(!)" "TEST(!)" "|" "DONE(!)")))
  (setq org-todo-keyword-faces
        '(
          ("TODO" . "red")
          ("DOING" . "dark orange")
          ("TEST" . "gold")
          ("DONE" . "dark green")
          ("NOGO" . "grey")
          ("KO" . "grey")
          ("WAITING" . "grey")
          ))
  (setq org-columns-default-format "%5TODO %40ITEM(Task) %20DEADLINE %4Effort(E){+} %4EffortDone(C){+}")
  (setq org-startup-indented t)
  (setq org-startup-folded 'content)
  (add-hook 'org-mode-hook 'org-indent-mode)
                                        ; tempo allows to insert blocks using abbrevs such as <s for code blocks or <q for quotes 
  (require 'org-tempo)
  (setq org-export-exclude-tags '("noexport" "draft"))
  (setq org-use-tag-inheritance org-export-exclude-tags)
  )

3.6.2 org babel FTW

Babel is the best part of emacs: literate programming, including executable source blocks in a document and input / output data off them.

(use-package ob-async)
(use-package ob-go)
(setq org-confirm-babel-evaluate nil)
(org-babel-do-load-languages
 'org-babel-load-languages
 '(
   (plantuml . t)
   (dot . t)
   (js . t)
   (python . t)
   (emacs-lisp . t)
   (go . t)
   (shell . t)
   ))
(setq org-babel-python-command "/usr/bin/python3")
(setq org-src-tab-acts-natively t)
(setq org-file-apps
      '((auto-mode . emacs)
        ("\\.x?html?\\'" . "firefox %s")
        ("\\.pdf\\'" . "zathura %s")))

3.6.3 org mode agenda

The idea would be to export what is planned/scheduled/deadlined and what has been done, to be used from mobile / web.

;; Find all org files via ag, much faster than the rest
(setq org-agenda-tag-filter-preset '("-ARCHIVE" "-IGNORE"))
(setq org-agenda-files
      (split-string
       (shell-command-to-string
        "ag -g \".org$\" ~/src/ ~/org/ ~/Documents/ ~/go/src/git.jardinmagique.info ~/go/src/gitlab.com/jeromenerf ~/go/src/github.com/jeromenerf")
       "\n"
       ))

;; Export all org files events to one "events" calendar, using scheduled and deadlines
(setq org-icalendar-combined-agenda-file "~/org/events.ics"
      org-icalendar-use-deadline '(todo-due event-if-todo event-if-not-todo)
      org-icalendar-use-scheduled '(todo-start event-if-todo event-if-not-todo)
      org-agenda-default-appointment-duration 60)

;; Export all org files logs to one "logs" calendar, using clock entries and todo status changes

3.6.4 org mode complete

(use-package org-ac
  :init(progn
         (require 'org-ac)
         (org-ac/config-default)
         ))

3.6.5 org mode pandoc export

(use-package ox-pandoc
  :init
  (setq org-pandoc-options-for-latex-pdf '(
                                        ;(columns . 25)
                                           (number-sections . t)
                                           (pdf-engine . "xelatex")
                                           (lua-filter . "/home/jerome/Documents/templates/shared/org-table-width-fix.lua")
                                           (template . "/home/jerome/Documents/templates/shared/default.latex")
                                           ))
  )

3.6.6 org mode reveals js for presentation

See https://github.com/yjwen/org-reveal/ README

(use-package ox-reveal
  :ensure t )

3.6.7 org and plantuml

Plantuml is like graphviz, with the added benefit of being easier to use.

(use-package plantuml-mode)
(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)
(setq org-plantuml-jar-path (expand-file-name "/usr/share/plantuml/plantuml.jar"))

3.6.8 org capture

(setq org-capture-templates
      '(("l" "Link" entry (file+headline "~/org/todo.org" "links")
         "* %? %^L %^g \n%T" :prepend t)
        ("t" "To Do" entry (file+headline "~/org/todo.org" "now")
         "* %? %^C\n%T" :prepend t)
        ))

Capture can be triggered from anywhere, using emacsclient -ne "(make-capture-frame)"

(defadvice org-capture-finalize
    (after delete-capture-frame activate)
  "Advise capture-finalize to close the frame."
  (if (equal "capture" (frame-parameter nil 'name))
      (delete-frame)))

(defadvice org-capture-destroy
    (after delete-capture-frame activate)
  "Advise capture-destroy to close the frame."
  (if (equal "capture" (frame-parameter nil 'name))
      (delete-frame)))

(use-package noflet
  :ensure t )
(defun make-capture-frame ()
  "Create a new frame and run 'org-capture'."
  (interactive)
  (make-frame '((window-system . x) (name . "capture")))
  (select-frame-by-name "capture")
  (delete-other-windows)
  (noflet ((switch-to-buffer-other-window (buf) (switch-to-buffer buf)))
    (org-capture)))

(setq org-capture-templates
      '(("t" "todo" entry (file+headline "~/todo.org" "Tasks")
         "* TODO [#A] %?\nSCHEDULED: %(org-insert-time-stamp (org-read-date nil t \"+0d\"))\n%a\n")))

3.6.9 org present

(use-package epresent
  :ensure t)

3.6.10 org mime, for emails, yeah

I don't want emacs to handle my emails, but I would gladly have it compose emails from on org file and send it through sendmail.

(setq send-mail-function 'sendmail-send-it )
  (use-package org-mime
        :ensure t
        :config
        (setq org-mime-library 'mml)
        )

3.6.11 org auto export html on save

I am not quite sure I actually want HTML files to be generated right there, with org files. Async export is mandatory though.

(setq org-export-in-background t)
;; (defun org-mode-export-hook ()
;;   (add-hook 'after-save-hook 'org-html-export-to-html t t))
;; (add-hook 'org-mode-hook #'org-mode-export-hook)

3.6.12 org publish

Hugo is driving me a bit mad and since I mainly use it to publish org files, I may as well try to publish my humble blog using emacs and org features.

What I need:

  • [X] all my org notes BUT the private ones (whitelist or blacklist?)
  • [X] an index page with a list of links, using the sitemap stuff, which is basic but enough.
  • [/] a semi decent CSS, as default yet readable as possible
  • [ ]

    an navigation system on top of the page, like top, prev, next and the main categories craft, hack, audiobooks…

    Nice to have:

    • [ ] tag links? pages? similar to hugo
(use-package htmlize)
(require 'ox-publish)
3.6.12.1 Preparing for the publication

I have not made up my mind on this. I still don't know if I want to delete everything before publication or not. I like the idea of delta publishing, faster, greener.

I may just have to remember to prune the dead branches once a year.

(defun blog/prepare (project-plist)
  "Remove previously generated content. Org doesn't do delta publishing."
  ;;(mapc #'delete-file (directory-files (plist-get project-plist :publishing-directory)))
  )

(defun blog/publish ()
  "Publish async, using the cache"
  (interactive)
  (org-publish-all nil t)
  )

(defun blog/republish-all ()
  "Publish async, from scratch"
  (interactive)
  (org-publish-all nil t)
  )
3.6.12.2 Filtering

Filtering the list of items to publish is the most important custom feature. To put simply, I would be OK with just publishing articles marked with an "blog" tag, wherever the article may be stored in the file hierarchy.

;; now that we have TAGS and FILETAGS
;; we want to filter out the files containing an EXCLUDE_TAG tags
;; if the file is a org file of course
(defun blog/tags-filter (orig-fun &rest args)
  (let ((files (apply orig-fun args)))
    (if
        (string= (car (car args)) "notes")
        (seq-filter
         (lambda (entry) (not
                          (seq-intersection
                           (org-publish-find-property entry :exclude-tags args 'html)
                           (org-publish-find-property entry :tags args 'blog-html)
                           )
                          )
           )
         files)
      files
      )
    )
  )

;; add an advice function around the generic function that harvest the project files.
(advice-add 'org-publish-get-base-files :around #'blog/tags-filter)

3.6.12.3 Navigation

The default navigation features are functional and simple enough for an hassle free experience. A basic UP and HOME buttons. No navigation per tag, categories, author, just the basic hierarchy.

;; HTML head extra content
(setq blog/html-head-extra
      (concat
       "<link rel=\"stylesheet\" type=\"text/css\" href=\"/org.css\" />"
       ))

;; HTML preamble
(setq blog/html-preamble
      (concat
       "<header><nav>"
       "<div class=\"home\"><a href='/'>Not much</a></div>"
       "<ul class=\"menu\">"
       "<li><a href='/hack'>Hack</a></li>"
       "<li><a href='/craft'>Craft</a></li>"
       "</ul>"
       "</nav></header>"))

;; HTML preamble
(setq blog/html-postamble
      (concat
       "<footer>"
       "<div>%a 2012.</div>"
       "<div>Created %d, last updated %C.</div>"
       "</footer>"))
3.6.12.4 Sitemap

Add some metadata to the html backend, such as TAGS, FILETAGS, etc. Maybe have a look at the org-export-registered-backends.

Once these information are added to a backend, it is possible to retrieve them with org-publish-find-property and add whatever information you need for your sitemap.

(org-export-define-derived-backend 'blog-html 'html
  :options-alist '(
                   (:tags "TAGS" nil nil split)
                   (:filetags "FILETAGS" nil nil split)
                   ))

;; sitemap header and iterator
(defun blog/sitemap (title list)
  "Generate the sitemap"
  (concat "#+TITLE: " title "\n" "\n"
          "#+HTML: <section class='sitemap'>\n"
          (string-join (mapcar #'car (cdr list)) "\n\n")
          "#+HTML: </section>")
  )

;; sitemap entry formatting
(defun blog/sitemap-entry (entry style project)
  "Sitemap (Blog Main Page) Entry Formatter"
  (when (not (directory-name-p entry))
    (format (string-join
             '("** [[file:%s][%s]]\n"
               "/%s/\n\n"
               "/%s/\n\n"
               ))
            entry
            (org-publish-find-title entry project)
            (or (org-publish-find-property entry :description project 'html) "")
            (format-time-string "%Y-%m-%d" (org-publish-find-date entry project))
            )))

NB:

  • tags: (mapconcat (lambda (x) (format "=%s=" x))(org-publish-find-property entry :tags project 'blog-html) " ")
3.6.12.5 Project settings
(setq org-html-coding-system 'utf-8-unix) ;; set encoding properly
(setq org-publish-use-timestamps-flag t) ;; disable to ignore cache and do full build instead
(setq org-publish-project-alist
      `(
        ("notes"
         :base-directory "~/org/"
         :base-extension "org"
         :exclude-tags ("noexport" "draft")
         :exclude "work/\\|todo.org"
         :publishing-directory "~/public_html/blog/"
         :preparation-function blog/prepare
         :publishing-function org-html-publish-to-html
         :recursive t
         :makeindex nil

         :table-of-contents t
         :section-numbers t
         :with-properties nil
         :with-tags t
         :with-date t
         :with-footnotes t

         :html-doctype "html5"
         :html-html5-fancy t
         :html-head nil
         :html-link-home "/"
         :html-home/up-format ""
         :html-head-include-default-style nil
         :html-head-include-scripts nil
         :html-viewport nil
         :html-self-link-headlines t
         :html-validation-link nil
         :html-head-extra ,blog/html-head-extra 
         :html-preamble ,blog/html-preamble
         :html-postamble ,blog/html-postamble

         :auto-sitemap         t
         :sitemap-filename     "sitemap.org"
         :sitemap-style list
         :sitemap-title ""
         :sitemap-function blog/sitemap
         :sitemap-format-entry blog/sitemap-entry
         :sitemap-sort-files   anti-chronologically
         )
        ("static"
         :base-directory "~/org/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|rss\\|js\\|json"
         :exclude "work/\\|img/"
         :publishing-directory "~/public_html/blog/"
         :recursive t
         :publishing-function org-publish-attachment
         )
        ("all" :components ("notes" "static"))
        ))

3.6.13 org create ids for all entries

Generating IDs for all entries could help linking to headings. In practice, it appears quite overkill.

(defun my/org-add-ids-to-headlines-in-file ()
  "Add ID properties to all headlines in the current file which
do not already have one."
  (interactive)
  (org-map-entries 'org-id-get-create))
                                        ; (add-hook 'org-mode-hook
                                        ;           (lambda ()
                                        ;             (add-hook 'before-save-hook 'my/org-add-ids-to-headlines-in-file nil 'local)))

3.7 magit, a git porcelain

(use-package magit
  :ensure t)

(use-package evil-magit
  :ensure t)

3.8 flycheck: lint, check, format

(use-package flycheck
  :diminish flycheck-mode
  :init
  (add-hook 'after-init-hook #'global-flycheck-mode))
(use-package smartparens
  :diminish smartparens-mode
  :ensure t
  :config
  (smartparens-global-mode 1))
(use-package evil-smartparens)

3.9 web browser integration

Set emacs to open links in my preferred browser, based on the URL

(setq
 browse-url-browser-function
 '(
   ("wrike\\.com" . browse-url-chrome)
   ("odoo" . browse-url-chrome)
   ("beend.i" . browse-url-chrome)
   ("." . browse-url-firefox)
   ))