Doom Emacs Configuration [OBSOLETE]
- This repository
- Intro
- Doom configuration files
- General Emacs settings
- Emacs daemon
- Package configuration
- User interface
- Highlight indent guides
- Editing
- Completion & IDE
- Debugging
- Symbols
- Natural languages
- System tools
- Features
- Fun
- Applications
- Programming
- Office
- System configuration
This repository
This repository (abougouffa/dotfiles) contains my configuration files for Zsh, Emacs, Vim, Alacritty and other Linux related stuff.
If you want to reuse some of these configurations, you will need to modify some directories and add some user specific information (usernames, passwords…)
This is the main configuration file .doom.d/config.org, (available also as a PDF file), it contains the literal configuration for Doom Emacs, and I use it to generate some other user configuration files (define aliases, environment variables, user tools, Git configuration…).
How to install
Since commit 55c92810, I’m using chezmoi to manage my Dotfiles.
Now the Dotfiles can be installed using the following command; however, I don’t recommend installing all of my dotfiles, try instead to adapt them or to copy some interesting chunks.
sudo pacman -S chezmoi
chezmoi init --apply abougouffa
Emacs stuff
To use my Doom Emacs configuration, you need first to install Doom Emacs to
~/.config/emacs
or .emacs.d
:
git clone https://github.com/doomemacs/doomemacs.git ~/.config/emacs
~/.config/emacs/bin/doom install
Until 12b3d20e, I was using Chemacs2 to manage multiple Emacs profiles. Since I’m using only Doom Emacs and Doom recently introduced a new feature to bootstrap other Emacs configs, so I switched to a plain Doom Emacs config.
Intro
I’ve been using Linux exclusively since 2010, GNU Emacs was always installed on my machine, but I didn’t discover the real Emacs until 2020, in the beginning, I started my Vanilla Emacs configuration from scratch, but after a while, it becomes a mess. As a new Emacs user, I didn’t understand the in the beginning how to optimize my configuration and how to do things correctly. I discovered then Spacemacs, which made things much easier, but it was a little slow, and just after, I found the awesome Doom Emacs, and since, I didn’t quit my Emacs screen!
In the beginning, I was basically copying chunks of Emacs Lisp code from the internet, which quickly becomes a mess, specially because I was using a mixture of vanilla Emacs style configurations and Doom style ones.
Now I decided to rewrite a cleaner version of my configuration which will be more Doom friendly, and for that, I found an excellent example in tecosaur’s emacs-config, so my current configuration is heavily inspired by tecosaur’s one.
My private Doom modules
I’m moving the big reusable configuration parts to separate modules. See the
.doom.d/modules/private
for the currently implemented modules.
This file
This is my literate configuration file, I use it to generate Doom’s config files
($DOOMDIR/init.el
, $DOOMDIR/packages.el
and $DOOMDIR/config.el
), as well as some
other shell scripts, app installers, app launchers… etc.
Make config.el
run (slightly) faster with lexical binding (see this blog post
for more info).
;;; config.el -*- coding: utf-8-unix; lexical-binding: t; -*-
Add the shebang and the description to the setup.sh
file, which will be used to
set system settings and install some missing dependencies.
#!/bin/bash
# This is an automatically generated setup file, it installes some missing
# dependencies, configure system services, set system settings form better
# desktop integration... etc.
# Abdelhak BOUGOUFFA (c) 2022
Add an initial comment to the ~/.zshrc
file.
# -*- mode: sh; -*-
# This file is automatically generated from my Org literate configuration.
# Abdelhak BOUGOUFFA (c) 2022
Doom configuration files
Pseudo early-init
This file will be loaded before the content of Doom’s private init.el
, I add
some special stuff which I want to load very early.
;;; pseudo-early-init.el -*- coding: utf-8-unix; lexical-binding: t; -*-
Useful functions
Here we define some useful functions, some of them are available via other
packages like cl-lib
, dash.el
or s.el
, but I don’t like to load too much third
party libraries, particulary in early stage, so let’s define here.
;;; === Primitives ===
;; (+bool "someval") ;; ==> t
(defun +bool (val) (not (null val)))
;;; === Higher order functions ===
;; (+foldr (lambda (a b) (message "(%d + %d)" a b) (+ a b)) 0 '(1 2 3 4 5)) ;; ==> 15
;; (5 + 0) -> (4 + 5) -> (3 + 9) -> (2 + 12) --> (1 + 14)
(defun +foldr (fun acc seq)
(if (null seq) acc
(funcall fun (car seq) (+foldr fun acc (cdr seq)))))
;; (+foldl (lambda (a b) (message "(%d + %d)" a b) (+ a b)) 0 '(1 2 3 4 5)) ;; ==> 15
;; (0 + 1) -> (1 + 2) -> (3 + 3) -> (6 + 4) -> (10 + 5)
(defun +foldl (fun acc seq)
(if (null seq) acc
(+foldl fun (funcall fun acc (car seq)) (cdr seq))))
;; (+all '(83 88 t "txt")) ;; ==> t
(defun +all (seq)
(+foldr (lambda (r l) (and r l)) t seq))
;; (+some '(nil nil "text" nil 2)) ;; ==> t
(defun +some (seq)
(+bool (+foldr (lambda (r l) (or r l)) nil seq)))
;; (+filter 'stringp '("A" 2 "C" nil 3)) ;; ==> ("A" "C")
(defun +filter (fun seq)
(when seq
(let ((head (car seq))
(tail (cdr seq)))
(if (funcall fun head)
(cons head (+filter fun tail))
(+filter fun tail)))))
;; (+zip '(1 2 3 4) '(a b c d) '("A" "B" "C" "D")) ;; ==> ((1 a "A") (2 b "B") (3 c "C") (4 d "D"))
(defun +zip (&rest seqs)
(if (null (car seqs)) nil
(cons (mapcar #'car seqs)
(apply #'+zip (mapcar #'cdr seqs)))))
;;; === Strings ===
;; (+str-join ", " '("foo" "10" "bar")) ;; ==> "foo, 10, bar"
(defun +str-join (sep seq)
(+foldl (lambda (l r) (concat l sep r))
(car seq) (cdr seq)))
;; (+str-split "foo, 10, bar" ", ") ;; ==> ("foo" "10" "bar")
(defun +str-split (str sep)
(let ((s (string-search sep str)))
(if s (cons (substring str 0 s)
(+str-split (substring str (+ s (length sep))) sep))
(list str))))
(defun +str-replace (old new s)
"Replaces OLD with NEW in S."
(replace-regexp-in-string (regexp-quote old) new s t t))
(defun +str-replace-all (replacements s)
"REPLACEMENTS is a list of cons-cells. Each `car` is replaced with `cdr` in S."
(replace-regexp-in-string (regexp-opt (mapcar 'car replacements))
(lambda (it) (cdr (assoc-string it replacements)))
s t t))
;;; === Files, IO ===
(defun +file-mime-type (file)
"Get MIME type for FILE based on magic codes provided by the 'file' command.
Return a symbol of the MIME type, ex: `text/x-lisp', `text/plain',
`application/x-object', `application/octet-stream', etc."
(let ((mime-type (shell-command-to-string (format "file --brief --mime-type %s" file))))
(intern (string-trim-right mime-type))))
(defun +file-name-incremental (filename)
"Return an unique file name for FILENAME.
If \"file.ext\" exists, returns \"file-0.ext\"."
(let* ((ext (file-name-extension filename))
(dir (file-name-directory filename))
(file (file-name-base filename))
(filename-regex (concat "^" file "\\(?:-\\(?1:[[:digit:]]+\\)\\)?" (if ext (concat "\\." ext) "")))
(last-file (car (last (directory-files dir nil filename-regex))))
(last-file-num (when (and last-file (string-match filename-regex last-file) (match-string 1 last-file))))
(num (1+ (string-to-number (or last-file-num "-1"))))
(filename (file-name-concat dir (format "%s%s%s" file (if last-file (format "-%d" num) "") (if ext (concat "." ext) "")))))
filename))
(defun +file-read-to-string (filename)
"Return a string with the contents of FILENAME."
(when (and (file-exists-p filename) (not (file-directory-p filename)))
(with-temp-buffer
(insert-file-contents filename)
(buffer-string))))
;;; === Systemd ===
(defun +systemd-running-p (service)
"Check if the systemd SERVICE is running."
(zerop (call-process "systemctl" nil nil nil "--user" "is-active" "--quiet" service ".service")))
(defun +systemd-command (service command &optional pre-fn post-fn)
"Call systemd with COMMAND and SERVICE."
(interactive)
(when pre-fn (funcall pre-fn))
(let ((success (zerop (call-process "systemctl" nil nil nil "--user" command service ".service"))))
(unless success
(message "[systemd]: Failed on calling '%s' on service %s.service." command service))
(when post-fn (funcall post-fn success))
success))
(defun +systemd-start (service &optional pre-fn post-fn)
"Start systemd SERVICE."
(interactive)
(+systemd-command service "start" pre-fn post-fn))
(defun +systemd-stop (service &optional pre-fn post-fn)
"Stops the systemd SERVICE."
(interactive)
(+systemd-command service "stop" pre-fn post-fn))
Fixes
;; Fixes to apply early
(when (daemonp)
;; When starting Emacs in daemon mode,
;; I need to have a valid passphrase in the gpg-agent.
(let ((try-again 3)
unlocked)
(while (not (or unlocked (zerop try-again)))
(setq unlocked (zerop (shell-command "gpg -q --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg > /dev/null" nil nil))
try-again (1- try-again))
(unless unlocked
(message "GPG: failed to unlock, please try again (%d)" try-again)))
(unless unlocked ;; Exit Emacs, systemd will restart it
(kill-emacs 1))))
Check for external tools
Some added packages require external tools, I like to check for these tools and store the result in global constants.
(defconst EAF-DIR (expand-file-name "eaf/eaf-repo" doom-data-dir))
(defconst IS-LUCID (string-search "LUCID" system-configuration-features))
(defconst FRICAS-DIR "/usr/lib/fricas/emacs")
(defconst AG-P (executable-find "ag"))
(defconst EAF-P (and (not IS-LUCID) (file-directory-p EAF-DIR)))
(defconst MPD-P (+all (mapcar #'executable-find '("mpc" "mpd"))))
(defconst MPV-P (executable-find "mpv"))
(defconst REPO-P (executable-find "repo"))
(defconst FRICAS-P (and (executable-find "fricas") (file-directory-p FRICAS-DIR)))
(defconst MAXIMA-P (executable-find "maxima"))
(defconst TUNTOX-P (executable-find "tuntox"))
(defconst ROSBAG-P (executable-find "rosbag"))
(defconst ZOTERO-P (executable-find "zotero"))
(defconst CHEZMOI-P (executable-find "chezmoi"))
(defconst STUNNEL-P (executable-find "stunnel"))
(defconst ECRYPTFS-P (+all (mapcar #'executable-find '("ecryptfs-add-passphrase" "/sbin/mount.ecryptfs_private"))))
(defconst BITWARDEN-P (executable-find "bw"))
(defconst YOUTUBE-DL-P (+some (mapcar #'executable-find '("yt-dlp" "youtube-dl"))))
(defconst NETEXTENDER-P (and (executable-find "netExtender") (+all (mapcar #'file-exists-p '("~/.local/bin/netextender" "~/.ssh/sslvpn.gpg")))))
(defconst CLANG-FORMAT-P (executable-find "clang-format"))
(defconst LANGUAGETOOL-P (executable-find "languagetool"))
Doom modules (init.el
)
Here is the literate configuration which generates the Doom’s init.el
file, this
file contains all the enabled Doom modules with the appropriate flags.
This section defines the default source blocks arguments . All source blocks in this section inherits these headers, so they will not be tangled unless overwriting in the block’s header.
File skeleton
This first section defines the template for the subsections, it uses the no-web
syntax to include subsections specified as <<sub-section-name>>
.
;;; init.el -*- coding: utf-8-unix; lexical-binding: t; -*-
;; This file controls what Doom modules are enabled and what order they load in.
;; Press 'K' on a module to view its documentation, and 'gd' to browse its directory.
;; I add some special stuff wich I want to load very early.
(load! "pseudo-early-init.el")
(doom!
:input
<<doom-input>>
:completion
<<doom-completion>>
:ui
<<doom-ui>>
:editor
<<doom-editor>>
:emacs
<<doom-emacs>>
:term
<<doom-term>>
:checkers
<<doom-checkers>>
:tools
<<doom-tools>>
:os
<<doom-os>>
:lang
<<doom-lang>>
:email
<<doom-email>>
:app
<<doom-app>>
:config
<<doom-config>>
:private
<<doom-private>>
)
Input (:input
)
Enable bidirectional languages support (bidi
).
bidi
General (:config
)
Enable literate
configuration (like this file!), and some defaults.
literate
(default +bindings +smartparens)
Completion (:completion
)
I’m lazy, I like Emacs to complete my writings.
(vertico +icons)
(company +childframe)
User interface (:ui
)
Enables some user interface features for better user experience, the beautiful
modeline
, the treemacs
project tree, better version control integration with
vc-gutter
… and other useful stuff.
zen
deft
doom
hydra
hl-todo
ophints
modeline
nav-flash
workspaces
indent-guides
doom-dashboard
(treemacs +lsp)
;; (ligatures +extra)
(popup +all +defaults)
(emoji +ascii +github)
(window-select +numbers)
(vc-gutter +diff-hl +pretty)
Editor (:editor
)
Some editing modules, the most important feature is EVIL to enable Vim style
editing in Emacs. I like also to edit with multiple cursors, enable yasnippet
support, wrap long lines, auto format support.
(evil +everywhere)
file-templates
fold
format
multiple-cursors
parinfer
snippets
word-wrap
Emacs builtin stuff (:emacs
)
Beautify Emacs builtin packages.
vc
undo
(ibuffer +icons)
Terminals (:term
)
Run commands in terminal from Emacs. I use mainly vterm
on my local machine,
however, I like to have eshell
, shell
and term
installed to use them for remote
file editing (via Tramp).
term
vterm
shell
eshell
Checkers (:checkers
)
I like to check my documents for errors while I’m typing. The grammar
module
enables LanguageTool support.
(syntax +childframe)
(spell +aspell)
Tools (:tools
)
I enable some useful tools which facilitate my work flow, I like to enable
Docker support, EditorConfig is a good feature to have. I like to enable
lsp-mode
and dap-mode
for coding and debugging by enabling the lsp
and debugger
modules with +lsp
support (further customization for lsp
and dap
below). pdf
adds support through pdf-tools
, which are great for viewing PDF files inside
Emacs, I also enable some extra tools, like magit
, lookup
, tmux
… etc.
ein
pdf
rgb
gist
make
tmux
direnv
upload
biblio
tree-sitter
editorconfig
(lsp +peek)
(docker +lsp)
(magit +forge)
(debugger +lsp)
(eval +overlay)
(lookup +docsets +dictionary +offline)
Operating system (:os
)
I enable tty
for better support of terminal editing.
(tty +osc)
Language support (:lang
)
Most of the projects I’m working on are mainly written in C/C++, Python, Rust
and some Lisp stuff, I edit also a lot of configuration and data files in
several formats (csv
, yaml
, xml
, json
, shell
scripts…). I use Org-mode to
manage all my papers and notes, so I need to enable as many features as I need,
I do enable plantuml
also to quickly plot UML models withing Org documents.
qt
data
plantuml
emacs-lisp
common-lisp
(ess +lsp)
(yaml +lsp)
(markdown +grip)
(csharp +dotnet)
(racket +lsp +xp)
(lua +lsp +fennel)
(web +tree-sitter)
(latex +lsp +latexmk)
(cc +lsp +tree-sitter)
(sh +lsp +tree-sitter)
(json +lsp +tree-sitter)
(rust +lsp +tree-sitter)
(julia +lsp +tree-sitter)
(python +lsp +pyenv +pyright +tree-sitter)
(scheme +chez +mit +chicken +gauche +guile +chibi)
(org +dragndrop +gnuplot +jupyter +pandoc +noter +journal +hugo +present +pomodoro +roam2)
Email (:email
)
I like to use mu4e
to manage mail mailboxes. The +org
flag adds org-msg
support
and +gmail
adds better management of Gmail accounts.
(:if (executable-find "mu") (mu4e +org +gmail))
Apps (:app
)
Emacs contains a ton of applications, some of them are supported by Doom, I like
to use Emacs manage my calendar, chat on IRC, and receive news. I do use EMMS
sometimes to play music without leaving Emacs, and I like to enable support for
emacs-everywhere
.
irc
rss
emms
calendar
everywhere
Private
(grammar +lsp)
;; (corfu +icons)
(binary +disasm)
(dired-ng +icons +bindings)
Additional packages (packages.el
)
This section generates Doom’s packages.el
, with the associated configurations
(use-package!
blocks).
This file shouldn’t be byte compiled.
;; -*- coding: utf-8-unix; no-byte-compile: t; -*-
General Emacs settings
User information
(setq user-full-name "Abdelhak Bougouffa"
user-mail-address "abougouffa@fedoraproject.org")
Common variables
(defvar +my/lang-main "en")
(defvar +my/lang-secondary "fr")
(defvar +my/lang-mother-tongue "ar")
(defvar +my/biblio-libraries-list (list (expand-file-name "~/Zotero/library.bib")))
(defvar +my/biblio-storage-list (list (expand-file-name "~/Zotero/storage/")))
(defvar +my/biblio-notes-path (expand-file-name "~/PhD/bibliography/notes/"))
(defvar +my/biblio-styles-path (expand-file-name "~/Zotero/styles/"))
;; Set it early, to avoid creating "~/org" at startup
(setq org-directory "~/Dropbox/Org")
Secrets
Set the path to my GPG encrypted secrets. I like to set the cache expiry to nil
instead of the default 2 hours.
(setq auth-sources '("~/.authinfo.gpg")
auth-source-do-cache t
auth-source-cache-expiry 86400 ; All day, defaut is 2h (7200)
password-cache t
password-cache-expiry 86400)
(after! epa
(setq-default epa-file-encrypt-to '("F808A020A3E1AC37")))
Better defaults
File deletion
Delete files by moving them to trash.
(setq-default delete-by-moving-to-trash t
trash-directory nil) ;; Use freedesktop.org trashcan
Window
Take new window space from all other windows (not just current).
(setq-default window-combination-resize t)
Split defaults
Split horizontally to right, vertically below the current window.
(setq evil-vsplit-window-right t
evil-split-window-below t)
Show list of buffers when splitting.
(defadvice! prompt-for-buffer (&rest _)
:after '(evil-window-split evil-window-vsplit)
(consult-buffer))
Messages buffer
Stick to buffer tail, useful with *Messages*
buffer. Derived from this answer.
(defvar +messages--auto-tail-enabled nil)
(defun +messages--auto-tail-a (&rest arg)
"Make *Messages* buffer auto-scroll to the end after each message."
(let* ((buf-name (buffer-name (messages-buffer)))
;; Create *Messages* buffer if it does not exist
(buf (get-buffer-create buf-name)))
;; Activate this advice only if the point is _not_ in the *Messages* buffer
;; to begin with. This condition is required; otherwise you will not be
;; able to use `isearch' and other stuff within the *Messages* buffer as
;; the point will keep moving to the end of buffer :P
(when (not (string= buf-name (buffer-name)))
;; Go to the end of buffer in all *Messages* buffer windows that are
;; *live* (`get-buffer-window-list' returns a list of only live windows).
(dolist (win (get-buffer-window-list buf-name nil :all-frames))
(with-selected-window win
(goto-char (point-max))))
;; Go to the end of the *Messages* buffer even if it is not in one of
;; the live windows.
(with-current-buffer buf
(goto-char (point-max))))))
(defun +messages-auto-tail-toggle ()
"Auto tail the '*Messages*' buffer."
(interactive)
(if +messages--auto-tail-enabled
(progn
(advice-remove 'message '+messages--auto-tail-a)
(setq +messages--auto-tail-enabled nil)
(message "+messages-auto-tail: Disabled."))
(advice-add 'message :after '+messages--auto-tail-a)
(setq +messages--auto-tail-enabled t)
(message "+messages-auto-tail: Enabled.")))
Undo and auto-save
Auto-save
(package! super-save
:disable t
:pin "3313f38ed7d23947992e19f1e464c6d544124144")
(use-package! super-save
:config
(setq auto-save-default t ;; nil to switch off the built-in `auto-save-mode', maybe leave it t to have a backup!
super-save-exclude '(".gpg")
super-save-remote-files nil
super-save-auto-save-when-idle t)
(super-save-mode +1))
(setq auto-save-default t) ;; enable built-in `auto-save-mode'
Undo
Tweak undo-fu
and other stuff from Doom’s :emacs undo
.
;; Increase undo history limits even more
(after! undo-fu
;; Emacs undo defaults
(setq undo-limit 10000000 ;; 1MB (default is 160kB, Doom's default is 400kB)
undo-strong-limit 100000000 ;; 100MB (default is 240kB, Doom's default is 3MB)
undo-outer-limit 1000000000) ;; 1GB (default is 24MB, Doom's default is 48MB)
;; Undo-fu customization options
(setq undo-fu-allow-undo-in-region t ;; Undoing with a selection will use undo within that region.
undo-fu-ignore-keyboard-quit t)) ;; Use the `undo-fu-disable-checkpoint' command instead of Ctrl-G `keyboard-quit' for non-linear behavior.
;; Evil undo
(after! evil
(setq evil-want-fine-undo t)) ;; By default while in insert all changes are one big blob
Visual undo (vundo
)
(package! vundo
:recipe (:host github
:repo "casouri/vundo")
:pin "16a09774ddfbd120d625cdd35fcf480e76e278bb")
(use-package! vundo
:defer t
:init
(defconst +vundo-unicode-symbols
'((selected-node . ?●)
(node . ?○)
(vertical-stem . ?│)
(branch . ?├)
(last-branch . ?╰)
(horizontal-stem . ?─)))
(map! :leader
(:prefix ("o")
:desc "vundo" "v" #'vundo))
:config
(setq vundo-glyph-alist +vundo-unicode-symbols
vundo-compact-display t
vundo-window-max-height 6))
Editing
;; Stretch cursor to the glyph width
(setq-default x-stretch-cursor t)
;; Enable relative line numbers
(setq display-line-numbers-type 'relative)
;; Iterate through CamelCase words
(global-subword-mode 1)
Emacs sources
(setq source-directory
(expand-file-name "~/Softwares/src/emacs/"))
Frame
Focus created frame
The problem is, every time I launch an Emacs frame (from KDE), Emacs starts
with no focus, I need each time to Alt-TAB
to get Emacs under focus, and then
start typing. I tried changing this behavior from Emacs by hooking raise-frame
at startup, but it didn’t work.
Got from this comment, not working on my Emacs version.
;; NOTE: Not tangled, not working
(add-hook 'server-switch-hook #'raise-frame)
After some investigations, I found that this issue is probably KDE specific, the issue goes away by setting: Window Management > Window Behavior > Focus > Focus stealing prevention to None in the KDE Settings.
Browsers
(setq browse-url-chrome-program "brave")
Emacs daemon
Initialization
(defun +daemon-startup ()
;; mu4e
(when (require 'mu4e nil t)
;; Automatically start `mu4e' in background.
(when (load! "mu-lock.el" (expand-file-name "email/mu4e/autoload" doom-modules-dir) t)
(setq +mu4e-lock-greedy t
+mu4e-lock-relaxed t)
(when (+mu4e-lock-available t)
;; Check each 5m, if `mu4e' if closed, start it in background.
(run-at-time nil ;; Launch now
(* 60 5) ;; Check each 5 minutes
(lambda ()
(when (and (not (mu4e-running-p)) (+mu4e-lock-available))
(mu4e--start)
(message "Started `mu4e' in background.")))))))
;; RSS
(when (require 'elfeed nil t)
(run-at-time nil (* 2 60 60) #'elfeed-update))) ;; Check every 2h
(when (daemonp)
;; At daemon startup
(add-hook 'emacs-startup-hook #'+daemon-startup)
;; After creating a new frame (via emacsclient)
;; Reload Doom's theme
(add-hook 'server-after-make-frame-hook #'doom/reload-theme))
Tweaks
Save recent files
When editing files with Emacs client, the files does not get stored by recentf
,
making Emacs forgets about recently opened files. A quick fix is to hook the
recentf-save-list
command to the delete-frame-functions
and delete-terminal-functions
which gets executed each time a
frame/terminal is deleted.
(when (daemonp)
(add-hook! '(delete-frame-functions delete-terminal-functions)
(let ((inhibit-message t))
(recentf-save-list)
(savehist-save))))
Package configuration
User interface
Font
Doom exposes five (optional) variables for controlling fonts in Doom. Here are
the three important ones: doom-font
, doom-unicode-font
and
doom-variable-pitch-font
. The doom-big-font
is used for doom-big-font-mode
; use
this for presentations or streaming.
They all accept either a font-spec
, font string ("Input Mono-12"
), or xlfd
font string. You generally only need these two:
Some good fonts:
Iosevka Fixed
(THE FONT)- Nerd fonts
FantasqueSansMono Nerd Font Mono
mononoki Nerd Font Mono
CaskaydiaCove Nerd Font Mono
Cascadia Code
Fantasque Sans Mono
JuliaMono
(good Unicode support)IBM Plex Mono
JetBrains Mono
Roboto Mono
Source Code Pro
Input Mono Narrow
Fira Code
(setq doom-font (font-spec :family "Iosevka Fixed Curly Slab" :size 20)
doom-big-font (font-spec :family "Iosevka Fixed Curly Slab" :size 30 :weight 'light)
doom-variable-pitch-font (font-spec :family "Lato")
doom-unicode-font (font-spec :family "JuliaMono")
doom-serif-font (font-spec :family "Iosevka Fixed Curly Slab" :weight 'light))
Theme
Doom
Set Doom’s theme, some good choices:
doom-one
(Atom like)doom-vibrant
(More vibrant version ofdoom-one
)doom-one-light
(Atom like)doom-dark+
(VS Code like)doom-xcode
(XCode like)doom-material
doom-material-dark
doom-palenight
doom-ayu-mirage
doom-monokai-pro
doom-tomorrow-day
doom-tomorrow-night
(setq doom-theme 'doom-one-light)
(remove-hook 'window-setup-hook #'doom-init-theme-h)
(add-hook 'after-init-hook #'doom-init-theme-h 'append)
Modus themes
(package! modus-themes :pin "ee35a9af344d2b2920589ec4d66e9cb513bdfb80")
(use-package! modus-themes
:init
(setq modus-themes-hl-line '(accented intense)
modus-themes-subtle-line-numbers t
modus-themes-region '(bg-only no-extend) ;; accented
modus-themes-variable-pitch-ui nil
modus-themes-fringes 'subtle
modus-themes-diffs nil
modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-intense-mouseovers t
modus-themes-paren-match '(bold intense)
modus-themes-syntax '(green-strings)
modus-themes-links '(neutral-underline background)
modus-themes-mode-line '(borderless padded)
modus-themes-tabs-accented nil ;; default
modus-themes-completions
'((matches . (extrabold intense accented))
(selection . (semibold accented intense))
(popup . (accented)))
modus-themes-headings '((1 . (rainbow 1.4))
(2 . (rainbow 1.3))
(3 . (rainbow 1.2))
(4 . (rainbow bold 1.1))
(t . (rainbow bold)))
modus-themes-org-blocks 'gray-background
modus-themes-org-agenda
'((header-block . (semibold 1.4))
(header-date . (workaholic bold-today 1.2))
(event . (accented italic varied))
(scheduled . rainbow)
(habit . traffic-light))
modus-themes-markup '(intense background)
modus-themes-mail-citations 'intense
modus-themes-lang-checkers '(background))
(defun +modus-themes-tweak-packages ()
(modus-themes-with-colors
(set-face-attribute 'cursor nil :background (modus-themes-color 'blue))
(set-face-attribute 'font-lock-type-face nil :foreground (modus-themes-color 'magenta-alt))
(custom-set-faces
;; Tweak `evil-mc-mode'
`(evil-mc-cursor-default-face ((,class :background ,magenta-intense-bg)))
;; Tweak `git-gutter-mode'
`(git-gutter-fr:added ((,class :foreground ,green-fringe-bg)))
`(git-gutter-fr:deleted ((,class :foreground ,red-fringe-bg)))
`(git-gutter-fr:modified ((,class :foreground ,yellow-fringe-bg)))
;; Tweak `doom-modeline'
`(doom-modeline-evil-normal-state ((,class :foreground ,green-alt-other)))
`(doom-modeline-evil-insert-state ((,class :foreground ,red-alt-other)))
`(doom-modeline-evil-visual-state ((,class :foreground ,magenta-alt)))
`(doom-modeline-evil-operator-state ((,class :foreground ,blue-alt)))
`(doom-modeline-evil-motion-state ((,class :foreground ,blue-alt-other)))
`(doom-modeline-evil-replace-state ((,class :foreground ,yellow-alt)))
;; Tweak `diff-hl-mode'
`(diff-hl-insert ((,class :foreground ,green-fringe-bg)))
`(diff-hl-delete ((,class :foreground ,red-fringe-bg)))
`(diff-hl-change ((,class :foreground ,yellow-fringe-bg)))
;; Tweak `solaire-mode'
`(solaire-default-face ((,class :inherit default :background ,bg-alt :foreground ,fg-dim)))
`(solaire-line-number-face ((,class :inherit solaire-default-face :foreground ,fg-unfocused)))
`(solaire-hl-line-face ((,class :background ,bg-active)))
`(solaire-org-hide-face ((,class :background ,bg-alt :foreground ,bg-alt)))
;; Tweak `display-fill-column-indicator-mode'
`(fill-column-indicator ((,class :height 0.3 :background ,bg-inactive :foreground ,bg-inactive)))
;; Tweak `mmm-mode'
`(mmm-cleanup-submode-face ((,class :background ,yellow-refine-bg)))
`(mmm-code-submode-face ((,class :background ,bg-active)))
`(mmm-comment-submode-face ((,class :background ,blue-refine-bg)))
`(mmm-declaration-submode-face ((,class :background ,cyan-refine-bg)))
`(mmm-default-submode-face ((,class :background ,bg-alt)))
`(mmm-init-submode-face ((,class :background ,magenta-refine-bg)))
`(mmm-output-submode-face ((,class :background ,red-refine-bg)))
`(mmm-special-submode-face ((,class :background ,green-refine-bg))))))
(add-hook 'modus-themes-after-load-theme-hook #'+modus-themes-tweak-packages)
:config
(modus-themes-load-operandi)
(map! :leader
:prefix "t" ;; toggle
:desc "Toggle Modus theme" "m" #'modus-themes-toggle))
Ef (εὖ) themes
(package! ef-themes :pin "3f9628750f8ff544169d4924e8c51f49b31f39e1")
(use-package! ef-themes
:unless t ;; Disabled
;; If you like two specific themes and want to switch between them, you
;; can specify them in `ef-themes-to-toggle' and then invoke the command
;; `ef-themes-toggle'. All the themes are included in the variable
;; `ef-themes-collection'.
:config
(setq ef-themes-to-toggle '(ef-light ef-day))
;; Make customisations that affect Emacs faces BEFORE loading a theme
;; (any change needs a theme re-load to take effect).
(setq ef-themes-headings ; read the manual's entry or the doc string
'((0 . (variable-pitch light 1.9))
(1 . (variable-pitch light 1.8))
(2 . (variable-pitch regular 1.7))
(3 . (variable-pitch regular 1.6))
(4 . (variable-pitch regular 1.5))
(5 . (variable-pitch 1.4)) ; absence of weight means `bold'
(6 . (variable-pitch 1.3))
(7 . (variable-pitch 1.2))
(t . (variable-pitch 1.1))))
;; They are nil by default...
(setq ef-themes-mixed-fonts t
ef-themes-variable-pitch-ui t)
;; ;; Disable all other themes to avoid awkward blending:
;; (mapc #'disable-theme custom-enabled-themes)
;; ;; Load the theme of choice:
;; (load-theme 'ef-light :no-confirm)
;; OR use this to load the theme which also calls `ef-themes-post-load-hook':
(ef-themes-select 'ef-light))
;; The themes we provide:
;;
;; Light: `ef-day', `ef-duo-light', `ef-light', `ef-spring', `ef-summer', `ef-trio-light'.
;; Dark: `ef-autumn', `ef-duo-dark', `ef-dark', `ef-night', `ef-trio-dark', `ef-winter'.
;;
;; Also those which are optimized for deuteranopia (red-green color
;; deficiency): `ef-deuteranopia-dark', `ef-deuteranopia-light'.
;; We also provide these commands, but do not assign them to any key:
;;
;; - `ef-themes-toggle'
;; - `ef-themes-select'
;; - `ef-themes-load-random'
;; - `ef-themes-preview-colors'
;; - `ef-themes-preview-colors-current'
Lambda themes
(package! lambda-themes
:disable t
:recipe (:host github
:repo "Lambda-Emacs/lambda-themes")
:pin "3313f38ed7d23947992e19f1e464c6d544124144")
(package! lambda-line
:disable t
:recipe (:host github
:repo "Lambda-Emacs/lambda-line"))
(use-package! lambda-themes
:init
(setq lambda-themes-set-italic-comments t
lambda-themes-set-italic-keywords t
lambda-themes-set-variable-pitch t
lambda-themes-set-evil-cursors t)
:config
;; load preferred theme
(load-theme 'lambda-light-faded t))
(use-package! lambda-line
:custom
(lambda-line-position 'top) ;; Set position of status-line
(lambda-line-abbrev t) ;; abbreviate major modes
(lambda-line-hspace " ") ;; add some cushion
(lambda-line-prefix t) ;; use a prefix symbol
(lambda-line-prefix-padding nil) ;; no extra space for prefix
(lambda-line-status-invert nil) ;; no invert colors
(lambda-line-gui-ro-symbol " ⨂") ;; symbols
(lambda-line-gui-mod-symbol " ⬤")
(lambda-line-gui-rw-symbol " ◯")
(lambda-line-space-top +.50) ;; padding on top and bottom of line
(lambda-line-space-bottom -.50)
(lambda-line-symbol-position 0.1) ;; adjust the vertical placement of symbol
:config
;; activate lambda-line
(lambda-line-mode)
;; set divider line in footer
(when (eq lambda-line-position 'top)
(setq-default mode-line-format (list "%_"))
(setq mode-line-format (list "%_"))))
Mode line
Clock
Display time and set the format to 24h.
(after! doom-modeline
(setq display-time-string-forms
'((propertize (concat " 🕘 " 24-hours ":" minutes))))
(display-time-mode 1) ; Enable time in the mode-line
;; Add padding to the right
(doom-modeline-def-modeline 'main
'(bar workspace-name window-number modals matches follow buffer-info remote-host buffer-position word-count parrot selection-info)
'(objed-state misc-info persp-name battery grip irc mu4e gnus github debug repl lsp minor-modes input-method indent-info buffer-encoding major-mode process vcs checker " ")))
Battery
Show battery level unless battery is not present or battery information is unknown.
(after! doom-modeline
(let ((battery-str (battery)))
(unless (or (equal "Battery status not available" battery-str)
(string-match-p (regexp-quote "unknown") battery-str)
(string-match-p (regexp-quote "N/A") battery-str))
(display-battery-mode 1))))
Mode line customization
(after! doom-modeline
(setq doom-modeline-bar-width 4
doom-modeline-mu4e t
doom-modeline-major-mode-icon t
doom-modeline-major-mode-color-icon t
doom-modeline-buffer-file-name-style 'truncate-upto-project))
Set transparency
;; NOTE: Not tangled
(set-frame-parameter (selected-frame) 'alpha '(85 100))
(add-to-list 'default-frame-alist '(alpha 97 100))
Dashboard
Custom splash image
Change the logo to an image, a set of beautiful images can be found in assets
.
File |
---|
emacs-e.svg |
gnu-emacs-white.svg |
gnu-emacs-flat.svg |
blackhole-lines.svg |
doom-emacs-white.svg |
doom-emacs-dark.svg |
doom-emacs-gray.svg |
(setq fancy-splash-image (expand-file-name "assets/doom-emacs-gray.svg" doom-user-dir))
Dashboard
(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-shortmenu)
(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-footer)
(add-hook! '+doom-dashboard-mode-hook (hl-line-mode -1))
(setq-hook! '+doom-dashboard-mode-hook evil-normal-state-cursor (list nil))
Which key
Make which-key
popup faster.
(setq which-key-idle-delay 0.5 ;; Default is 1.0
which-key-idle-secondary-delay 0.05) ;; Default is nil
I’ve stolen this chunk (like many others) from tecosaur’s config, it helps to
replace the evil-
prefix with a unicode symbol, making which-key
’s candidate
list less verbose.
(setq which-key-allow-multiple-replacements t)
(after! which-key
(pushnew! which-key-replacement-alist
'(("" . "\\`+?evil[-:]?\\(?:a-\\)?\\(.*\\)") . (nil . "🅔·\\1"))
'(("\\`g s" . "\\`evilem--?motion-\\(.*\\)") . (nil . "Ⓔ·\\1"))))
Window title
I’d like to have just the buffer name, then if applicable the project folder.
(setq frame-title-format
'(""
(:eval
(if (s-contains-p org-roam-directory (or buffer-file-name ""))
(replace-regexp-in-string ".*/[0-9]*-?" "☰ "
(subst-char-in-string ?_ ?\s buffer-file-name))
"%b"))
(:eval
(when-let* ((project-name (projectile-project-name))
(project-name (if (string= "-" project-name)
(ignore-errors (file-name-base (string-trim-right (vc-root-dir))))
project-name)))
(format (if (buffer-modified-p) " ○ %s" " ● %s") project-name)))))
SVG tag and svg-lib
(package! svg-tag-mode :pin "efd22edf650fb25e665269ba9fed7ccad0771a2f")
(use-package! svg-tag-mode
:commands svg-tag-mode
:config
(setq svg-tag-tags
'(("^\\*.* .* \\(:[A-Za-z0-9]+\\)" .
((lambda (tag)
(svg-tag-make
tag
:beg 1
:font-family "Roboto Mono"
:font-size 10
:height 0.8
:padding 0
:margin 0))))
("\\(:[A-Za-z0-9]+:\\)$" .
((lambda (tag)
(svg-tag-make
tag
:beg 1
:end -1
:font-family "Roboto Mono"
:font-size 10
:height 0.8
:padding 0
:margin 0)))))))
(after! svg-lib
;; Set `svg-lib' cache directory
(setq svg-lib-icons-dir (expand-file-name "svg-lib" doom-data-dir)))
Focus
Dim the font color of text in surrounding paragraphs, focus only on the current line.
(package! focus :pin "9dd85fc474bbc1ebf22c287752c960394fcd465a")
(use-package! focus
:commands focus-mode)
Scrolling
(package! good-scroll
:disable EMACS29+
:pin "a7ffd5c0e5935cebd545a0570f64949077f71ee3")
(use-package! good-scroll
:unless EMACS29+
:config (good-scroll-mode 1))
(when EMACS29+
(pixel-scroll-precision-mode 1))
(setq hscroll-step 1
hscroll-margin 0
scroll-step 1
scroll-margin 0
scroll-conservatively 101
scroll-up-aggressively 0.01
scroll-down-aggressively 0.01
scroll-preserve-screen-position 'always
auto-window-vscroll nil
fast-but-imprecise-scrolling nil)
All the icons
Set some custom icons for some file extensions, basically for .m
files.
(after! all-the-icons
(setcdr (assoc "m" all-the-icons-extension-icon-alist)
(cdr (assoc "matlab" all-the-icons-extension-icon-alist))))
Tabs
(after! centaur-tabs
;; For some reason, setting `centaur-tabs-set-bar' this to `right'
;; instead of Doom's default `left', fixes this issue with Emacs daemon:
;; https://github.com/doomemacs/doomemacs/issues/6647#issuecomment-1229365473
(setq centaur-tabs-set-bar 'right
centaur-tabs-gray-out-icons 'buffer
centaur-tabs-set-modified-marker t
centaur-tabs-close-button "⨂"
centaur-tabs-modified-marker "⨀"))
Zen (writeroom) mode
(after! writeroom-mode
;; Show mode line
(setq writeroom-mode-line t)
;; Disable line numbers
(add-hook! 'writeroom-mode-enable-hook
(when (bound-and-true-p display-line-numbers-mode)
(setq-local +line-num--was-activate-p display-line-numbers-type)
(display-line-numbers-mode -1)))
(add-hook! 'writeroom-mode-disable-hook
(when (bound-and-true-p +line-num--was-activate-p)
(display-line-numbers-mode +line-num--was-activate-p)))
(after! org
;; Increase latex previews scale in Zen mode
(add-hook! 'writeroom-mode-enable-hook (+org-format-latex-set-scale 2.0))
(add-hook! 'writeroom-mode-disable-hook (+org-format-latex-set-scale 1.4)))
(after! blamer
;; Disable blamer in zen (writeroom) mode
(add-hook! 'writeroom-mode-enable-hook
(when (bound-and-true-p blamer-mode)
(setq +blamer-mode--was-active-p t)
(blamer-mode -1)))
(add-hook! 'writeroom-mode-disable-hook
(when (bound-and-true-p +blamer-mode--was-active-p)
(blamer-mode 1)))))
Highlight indent guides
(after! highlight-indent-guides
(setq highlight-indent-guides-character ?│
highlight-indent-guides-responsive 'top))
Editing
File templates
For some file types, we can overwrite the defaults in the snippets’ directory.
(set-file-template! "\\.tex$" :trigger "__" :mode 'latex-mode)
(set-file-template! "\\.org$" :trigger "__" :mode 'org-mode)
(set-file-template! "/LICEN[CS]E$" :trigger '+file-templates/insert-license)
Scratch buffer
Tell the scratch buffer to start in emacs-lisp-mode
.
(setq doom-scratch-initial-major-mode 'emacs-lisp-mode)
Mouse buttons
Map extra mouse buttons to jump between buffers
(map! :n [mouse-8] #'better-jumper-jump-backward
:n [mouse-9] #'better-jumper-jump-forward)
;; Enable horizontal scrolling with the second mouse wheel or the touchpad
(setq mouse-wheel-tilt-scroll t
mouse-wheel-progressive-speed nil)
Very large files
The very large files mode loads large files in chunks, allowing one to open ridiculously large files.
(package! vlf :pin "cc02f2533782d6b9b628cec7e2dcf25b2d05a27c")
To make VLF available without delaying startup, we’ll just load it in quiet moments.
(use-package! vlf-setup
:defer-incrementally vlf-tune vlf-base vlf-write vlf-search vlf-occur vlf-follow vlf-ediff vlf)
Evil
(after! evil
;; This fixes https://github.com/doomemacs/doomemacs/issues/6478
;; Ref: https://github.com/emacs-evil/evil/issues/1630
(evil-select-search-module 'evil-search-module 'isearch)
(setq evil-kill-on-visual-paste nil)) ; Don't put overwritten text in the kill ring
(package! evil-escape :disable t)
Aggressive indent
(package! aggressive-indent :pin "70b3f0add29faff41e480e82930a231d88ee9ca7")
(use-package! aggressive-indent
:commands (aggressive-indent-mode))
YASnippet
Nested snippets are good, enable that.
(setq yas-triggers-in-field t)
Completion & IDE
Company
I do not find company
useful in Org files.
(setq company-global-modes
'(not erc-mode
circe-mode
message-mode
help-mode
gud-mode
vterm-mode
org-mode))
Tweak company-box
(after! company-box
(defun +company-box--reload-icons-h ()
(setq company-box-icons-all-the-icons
(let ((all-the-icons-scale-factor 0.8))
`((Unknown . ,(all-the-icons-faicon "code" :face 'all-the-icons-purple))
(Text . ,(all-the-icons-material "text_fields" :face 'all-the-icons-green))
(Method . ,(all-the-icons-faicon "cube" :face 'all-the-icons-red))
(Function . ,(all-the-icons-faicon "cube" :face 'all-the-icons-blue))
(Constructor . ,(all-the-icons-faicon "cube" :face 'all-the-icons-blue-alt))
(Field . ,(all-the-icons-faicon "tag" :face 'all-the-icons-red))
(Variable . ,(all-the-icons-material "adjust" :face 'all-the-icons-blue))
(Class . ,(all-the-icons-material "class" :face 'all-the-icons-red))
(Interface . ,(all-the-icons-material "tune" :face 'all-the-icons-red))
(Module . ,(all-the-icons-faicon "cubes" :face 'all-the-icons-red))
(Property . ,(all-the-icons-faicon "wrench" :face 'all-the-icons-red))
(Unit . ,(all-the-icons-material "straighten" :face 'all-the-icons-red))
(Value . ,(all-the-icons-material "filter_1" :face 'all-the-icons-red))
(Enum . ,(all-the-icons-material "plus_one" :face 'all-the-icons-red))
(Keyword . ,(all-the-icons-material "filter_center_focus" :face 'all-the-icons-red-alt))
(Snippet . ,(all-the-icons-faicon "expand" :face 'all-the-icons-red))
(Color . ,(all-the-icons-material "colorize" :face 'all-the-icons-red))
(File . ,(all-the-icons-material "insert_drive_file" :face 'all-the-icons-red))
(Reference . ,(all-the-icons-material "collections_bookmark" :face 'all-the-icons-red))
(Folder . ,(all-the-icons-material "folder" :face 'all-the-icons-red-alt))
(EnumMember . ,(all-the-icons-material "people" :face 'all-the-icons-red))
(Constant . ,(all-the-icons-material "pause_circle_filled" :face 'all-the-icons-red))
(Struct . ,(all-the-icons-material "list" :face 'all-the-icons-red))
(Event . ,(all-the-icons-material "event" :face 'all-the-icons-red))
(Operator . ,(all-the-icons-material "control_point" :face 'all-the-icons-red))
(TypeParameter . ,(all-the-icons-material "class" :face 'all-the-icons-red))
(Template . ,(all-the-icons-material "settings_ethernet" :face 'all-the-icons-green))
(ElispFunction . ,(all-the-icons-faicon "cube" :face 'all-the-icons-blue))
(ElispVariable . ,(all-the-icons-material "adjust" :face 'all-the-icons-blue))
(ElispFeature . ,(all-the-icons-material "stars" :face 'all-the-icons-orange))
(ElispFace . ,(all-the-icons-material "format_paint" :face 'all-the-icons-pink))))))
(when (daemonp)
;; Replace Doom defined icons with mine
(when (memq #'+company-box--load-all-the-icons server-after-make-frame-hook)
(remove-hook 'server-after-make-frame-hook #'+company-box--load-all-the-icons))
(add-hook 'server-after-make-frame-hook #'+company-box--reload-icons-h))
;; Reload icons even if not in Daemon mode
(+company-box--reload-icons-h))
Treemacs
(after! treemacs
(require 'dired)
;; My custom stuff (from tecosaur's config)
(setq +treemacs-file-ignore-extensions
'(;; LaTeX
"aux" "ptc" "fdb_latexmk" "fls" "synctex.gz" "toc"
;; LaTeX - bibliography
"bbl"
;; LaTeX - glossary
"glg" "glo" "gls" "glsdefs" "ist" "acn" "acr" "alg"
;; LaTeX - pgfplots
"mw"
;; LaTeX - pdfx
"pdfa.xmpi"
;; Python
"pyc"))
(setq +treemacs-file-ignore-globs
'(;; LaTeX
"*/_minted-*"
;; AucTeX
"*/.auctex-auto"
"*/_region_.log"
"*/_region_.tex"
;; Python
"*/__pycache__"))
;; Reload treemacs theme
(setq doom-themes-treemacs-enable-variable-pitch nil
doom-themes-treemacs-theme "doom-colors")
(doom-themes-treemacs-config)
(setq treemacs-show-hidden-files nil
treemacs-hide-dot-git-directory t
treemacs-width 30)
(defvar +treemacs-file-ignore-extensions '()
"File extension which `treemacs-ignore-filter' will ensure are ignored")
(defvar +treemacs-file-ignore-globs '()
"Globs which will are transformed to `+treemacs-file-ignore-regexps' which `+treemacs-ignore-filter' will ensure are ignored")
(defvar +treemacs-file-ignore-regexps '()
"RegExps to be tested to ignore files, generated from `+treeemacs-file-ignore-globs'")
(defun +treemacs-file-ignore-generate-regexps ()
"Generate `+treemacs-file-ignore-regexps' from `+treemacs-file-ignore-globs'"
(setq +treemacs-file-ignore-regexps (mapcar 'dired-glob-regexp +treemacs-file-ignore-globs)))
(unless (equal +treemacs-file-ignore-globs '())
(+treemacs-file-ignore-generate-regexps))
(defun +treemacs-ignore-filter (file full-path)
"Ignore files specified by `+treemacs-file-ignore-extensions', and `+treemacs-file-ignore-regexps'"
(or (member (file-name-extension file) +treemacs-file-ignore-extensions)
(let ((ignore-file nil))
(dolist (regexp +treemacs-file-ignore-regexps ignore-file)
(setq ignore-file (or ignore-file (if (string-match-p regexp full-path) t nil)))))))
(add-to-list 'treemacs-ignored-file-predicates #'+treemacs-ignore-filter))
Projectile
Doom Emacs defined a function (doom-project-ignored-p path)
and uses
it with projectile-ignored-project-function
. So we will create a
wrapper function which calls Doom’s one, with an extra check.
;; Run `M-x projectile-discover-projects-in-search-path' to reload paths from this variable
(setq projectile-project-search-path
'("~/PhD/papers"
"~/PhD/workspace"
"~/PhD/workspace-no"
"~/PhD/workspace-no/ez-wheel/swd-starter-kit-repo"
("~/Projects/foss" . 2))) ;; ("dir" . depth)
(setq projectile-ignored-projects
'("/tmp"
"~/"
"~/.cache"
"~/.doom.d"
"~/.emacs.d/.local/straight/repos/"))
(setq +projectile-ignored-roots
'("~/.cache"
;; No need for this one, as `doom-project-ignored-p' checks for files in `doom-local-dir'
"~/.emacs.d/.local/straight/"))
(defun +projectile-ignored-project-function (filepath)
"Return t if FILEPATH is within any of `+projectile-ignored-roots'"
(require 'cl-lib)
(or (doom-project-ignored-p filepath) ;; Used by default by doom with `projectile-ignored-project-function'
(cl-some (lambda (root) (file-in-directory-p (expand-file-name filepath) (expand-file-name root)))
+projectile-ignored-roots)))
(setq projectile-ignored-project-function #'+projectile-ignored-project-function)
Tramp
Let’s try to make tramp handle prompts better
(after! tramp
(setenv "SHELL" "/bin/bash")
(setq tramp-shell-prompt-pattern "\\(?:^\\|
\\)[^]#$%>\n]*#?[]#$%>] *\\(\\[[0-9;]*[a-zA-Z] *\\)*")) ;; default +
Eros-eval
This makes the result of evals slightly prettier.
(setq eros-eval-result-prefix "⟹ ")
dir-locals.el
Reload dir-locals.el
variables after modification. Taken from this answer.
(defun +dir-locals-reload-for-current-buffer ()
"reload dir locals for the current buffer"
(interactive)
(let ((enable-local-variables :all))
(hack-dir-local-variables-non-file-buffer)))
(defun +dir-locals-reload-for-all-buffers-in-this-directory ()
"For every buffer with the same `default-directory` as the
current buffer's, reload dir-locals."
(interactive)
(let ((dir default-directory))
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (equal default-directory dir)
(+dir-locals-reload-for-current-buffer))))))
(defun +dir-locals-enable-autoreload ()
(when (and (buffer-file-name)
(equal dir-locals-file (file-name-nondirectory (buffer-file-name))))
(message "Dir-locals will be reloaded after saving.")
(add-hook 'after-save-hook '+dir-locals-reload-for-all-buffers-in-this-directory nil t)))
(add-hook! '(emacs-lisp-mode-hook lisp-data-mode-hook) #'+dir-locals-enable-autoreload)
Language Server Protocol
Eglot
Eglot uses project.el
to detect the project root. This is a workaround to make
it work with projectile
:
(after! eglot
;; A hack to make it works with projectile
(defun projectile-project-find-function (dir)
(let* ((root (projectile-project-root dir)))
(and root (cons 'transient root))))
(with-eval-after-load 'project
(add-to-list 'project-find-functions 'projectile-project-find-function))
;; Use clangd with some options
(set-eglot-client! 'c++-mode '("clangd" "-j=3" "--clang-tidy")))
LSP mode
Unpin package
(unpin! lsp-mode)
Tweaks
Performance
Use
plist
instead of hash table, LSP mode needs to be reinstalled after setting this environment variable (see).export LSP_USE_PLISTS=true
(after! lsp-mode (setq lsp-idle-delay 1.0 lsp-log-io nil gc-cons-threshold (* 1024 1024 100))) ;; 100MiB
Features & UI
LSP mode provides a set of configurable UI stuff. By default, Doom Emacs disables some UI components; however, I like to enable some less intrusive, more useful UI stuff.
(after! lsp-mode (setq lsp-lens-enable t lsp-semantic-tokens-enable t ;; hide unreachable ifdefs lsp-enable-symbol-highlighting t lsp-headerline-breadcrumb-enable nil ;; LSP UI related tweaks lsp-ui-sideline-enable nil lsp-ui-sideline-show-hover nil lsp-ui-sideline-show-symbol nil lsp-ui-sideline-show-diagnostics nil lsp-ui-sideline-show-code-actions nil))
LSP mode with clangd
(after! lsp-clangd
(setq lsp-clients-clangd-args
'("-j=4"
"--background-index"
"--clang-tidy"
"--completion-style=detailed"
"--header-insertion=never"
"--header-insertion-decorators=0"))
(set-lsp-priority! 'clangd 1))
LSP mode with ccls
;; NOTE: Not tangled, using the default ccls
(after! ccls
(setq ccls-initialization-options
'(:index (:comments 2
:trackDependency 1
:threads 4)
:completion (:detailedLabel t)))
(set-lsp-priority! 'ccls 2)) ; optional as ccls is the default in Doom
Enable lsp
over tramp
Python
(after! tramp (when (require 'lsp-mode nil t) ;; (require 'lsp-pyright) (setq lsp-enable-snippet nil lsp-log-io nil ;; To bypass the "lsp--document-highlight fails if ;; textDocument/documentHighlight is not supported" error lsp-enable-symbol-highlighting nil) (lsp-register-client (make-lsp-client :new-connection (lsp-tramp-connection "pyls") :major-modes '(python-mode) :remote? t :server-id 'pyls-remote))))
C/C++ with
ccls
;; NOTE: WIP: Not tangled (after! tramp (when (require 'lsp-mode nil t) (require 'ccls) (setq lsp-enable-snippet nil lsp-log-io nil lsp-enable-symbol-highlighting t) (lsp-register-client (make-lsp-client :new-connection (lsp-tramp-connection (lambda () (cons ccls-executable ; executable name on remote machine 'ccls' ccls-args))) :major-modes '(c-mode c++-mode objc-mode cuda-mode) :remote? t :server-id 'ccls-remote))) (add-to-list 'tramp-remote-path 'tramp-own-remote-path))
C/C++ with
clangd
(after! tramp (when (require 'lsp-mode nil t) (setq lsp-enable-snippet nil lsp-log-io nil ;; To bypass the "lsp--document-highlight fails if ;; textDocument/documentHighlight is not supported" error lsp-enable-symbol-highlighting nil) (lsp-register-client (make-lsp-client :new-connection (lsp-tramp-connection (lambda () (cons "clangd-12" ; executable name on remote machine 'ccls' lsp-clients-clangd-args))) :major-modes '(c-mode c++-mode objc-mode cuda-mode) :remote? t :server-id 'clangd-remote))))
VHDL
By default, LSP uses the proprietary VHDL-Tool to provide LSP features; however,
there is free and open source alternatives: ghdl-ls
and rust_hdl
. I have some
issues running ghdl-ls
installed form pip
through the pyghdl
package, so let’s
use rust_hdl
instead.
(use-package! vhdl-mode
:when (and (modulep! :tools lsp) (not (modulep! :tools lsp +eglot)))
:hook (vhdl-mode . #'+lsp-vhdl-ls-load)
:init
(defun +lsp-vhdl-ls-load ()
(interactive)
(lsp t)
(flycheck-mode t))
:config
;; Required unless vhdl_ls is on the $PATH
(setq lsp-vhdl-server-path "~/Projects/foss/repos/rust_hdl/target/release/vhdl_ls"
lsp-vhdl-server 'vhdl-ls
lsp-vhdl--params nil)
(require 'lsp-vhdl))
SonarLint
(package! lsp-sonarlint
:disable t :pin "3313f38ed7d23947992e19f1e464c6d544124144")
(use-package! lsp-sonarlint)
Cppcheck
Check for everything!
(after! flycheck
(setq flycheck-cppcheck-checks '("information"
"missingInclude"
"performance"
"portability"
"style"
"unusedFunction"
"warning"))) ;; Actually, we can use "all"
Project CMake
A good new package to facilitate using CMake projects with Emacs, it glues
together project
, eglot
, cmake
and clangd
.
(package! project-cmake
:disable t ; (not (modulep! :tools lsp +eglot)) ; Enable only if (lsp +eglot) is used
:pin "3313f38ed7d23947992e19f1e464c6d544124144"
:recipe (:host github
:repo "juanjosegarciaripoll/project-cmake"))
(use-package! project-cmake
:config
(require 'eglot)
(project-cmake-scan-kits)
(project-cmake-eglot-integration))
Clang-format
(package! clang-format :pin "e48ff8ae18dc7ab6118c1f6752deb48cb1fc83ac")
(use-package! clang-format
:when CLANG-FORMAT-P
:commands (clang-format-region))
Auto-include C++ headers
(package! cpp-auto-include
:recipe (:host github
:repo "emacsorphanage/cpp-auto-include")
:pin "0ce829f27d466c083e78b9fe210dcfa61fb417f4")
(use-package! cpp-auto-include
:commands cpp-auto-include)
C/C++ preprocessor conditions
In LSP mode, I configure lsp-semantic-tokens-enable
to enable fading unreachable
#ifdef
blocks, in case LSP is disabled, there is a similar built-in mode for
Emacs to do so. However, for a fully satisfying experience, it needs more work
to take into account macros defined at compile time (using compile_commands.json
for example).
(unless (modulep! :lang cc +lsp) ;; Disable if LSP for C/C++ is enabled
(use-package! hideif
:hook (c-mode . hide-ifdef-mode)
:hook (c++-mode . hide-ifdef-mode)
:init
(setq hide-ifdef-shadow t
hide-ifdef-initially t)))
Erefactor
(package! erefactor
:recipe (:host github
:repo "mhayashi1120/Emacs-erefactor")
:pin "bfe27a1b8c7cac0fe054e76113e941efa3775fe8")
(use-package! erefactor
:defer t)
Lorem ipsum
(package! emacs-lorem-ipsum
:recipe (:host github
:repo "jschaf/emacs-lorem-ipsum")
:pin "da75c155da327c7a7aedb80f5cfe409984787049")
(use-package! lorem-ipsum
:commands (lorem-ipsum-insert-sentences
lorem-ipsum-insert-paragraphs
lorem-ipsum-insert-list))
Coverage test
(package! cov :pin "cd3e1995c596cc227124db9537792d8329ffb696")
Debugging
DAP
I like to use cpptools
over webfreak.debug
. So I enable it after loading
dap-mode
. I like also to have a mode minimal UI. And I like to trigger dap-hydra
when the program hits a break point, and automatically delete the session and
close Hydra when DAP is terminated.
(unpin! dap-mode)
(after! dap-mode
;; Set latest versions
(setq dap-cpptools-extension-version "1.11.5")
(require 'dap-cpptools)
(setq dap-codelldb-extension-version "1.7.4")
(require 'dap-codelldb)
(setq dap-gdb-lldb-extension-version "0.26.0")
(require 'dap-gdb-lldb)
;; More minimal UI
(setq dap-auto-configure-features '(breakpoints locals expressions tooltip)
dap-auto-show-output nil ;; Hide the annoying server output
lsp-enable-dap-auto-configure t)
;; Automatically trigger dap-hydra when a program hits a breakpoint.
(add-hook 'dap-stopped-hook (lambda (arg) (call-interactively #'dap-hydra)))
;; Automatically delete session and close dap-hydra when DAP is terminated.
(add-hook 'dap-terminated-hook
(lambda (arg)
(call-interactively #'dap-delete-session)
(dap-hydra/nil)))
;; A workaround to correctly show breakpoints
;; from: https://github.com/emacs-lsp/dap-mode/issues/374#issuecomment-1140399819
(add-hook! +dap-running-session-mode
(set-window-buffer nil (current-buffer))))
Doom store
Doom Emacs stores session information persistently using the core store
mechanism. However, relaunching a new session doesn’t overwrite the last stored
session, to do so, I define a helper function to clear data stored in the
"+debugger"
location. (see +debugger--get-last-config
function.)
(defun +debugger/clear-last-session ()
"Clear the last stored session"
(interactive)
(doom-store-clear "+debugger"))
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :tools debugger +lsp)
:prefix ("d" . "debugger")
:desc "Clear last DAP session" "c" #'+debugger/clear-last-session))
RealGUD
For C/C++, DAP mode is missing so much features. In my experience, both cpptools
and gdb
DAP interfaces aren’t mature, it stops and disconnect while debugging,
making it a double pain.
Additional commands
There is no better than using pure GDB, it makes debugging extremely flexible.
Let’s define some missing GDB commands, add them to Hydra keys, and define some
reverse debugging commands for usage with rr (which we can use by substituting
gdb
by rr replay
when starting a debug session).
(after! realgud
(require 'hydra)
;; Add some missing gdb/rr commands
(defun +realgud:cmd-start (arg)
"start = break main + run"
(interactive "p")
(realgud-command "start"))
(defun +realgud:cmd-reverse-next (arg)
"Reverse next"
(interactive "p")
(realgud-command "reverse-next"))
(defun +realgud:cmd-reverse-step (arg)
"Reverse step"
(interactive "p")
(realgud-command "reverse-step"))
(defun +realgud:cmd-reverse-continue (arg)
"Reverse continue"
(interactive "p")
(realgud-command "reverse-continue"))
(defun +realgud:cmd-reverse-finish (arg)
"Reverse finish"
(interactive "p")
(realgud-command "reverse-finish"))
;; Define a hydra binding
(defhydra realgud-hydra (:color pink :hint nil :foreign-keys run)
"
Stepping | _n_: next | _i_: step | _o_: finish | _c_: continue | _R_: restart | _u_: until-here
Revese | _rn_: next | _ri_: step | _ro_: finish | _rc_: continue |
Breakpts | _ba_: break | _bD_: delete | _bt_: tbreak | _bd_: disable | _be_: enable | _tr_: backtrace
Eval | _ee_: at-point | _er_: region | _eE_: eval |
| _!_: shell | _Qk_: kill | _Qq_: quit | _Sg_: gdb | _Ss_: start
"
("n" realgud:cmd-next)
("i" realgud:cmd-step)
("o" realgud:cmd-finish)
("c" realgud:cmd-continue)
("R" realgud:cmd-restart)
("u" realgud:cmd-until-here)
("rn" +realgud:cmd-reverse-next)
("ri" +realgud:cmd-reverse-step)
("ro" +realgud:cmd-reverse-finish)
("rc" +realgud:cmd-reverse-continue)
("ba" realgud:cmd-break)
("bt" realgud:cmd-tbreak)
("bD" realgud:cmd-delete)
("be" realgud:cmd-enable)
("bd" realgud:cmd-disable)
("ee" realgud:cmd-eval-at-point)
("er" realgud:cmd-eval-region)
("tr" realgud:cmd-backtrace)
("eE" realgud:cmd-eval)
("!" realgud:cmd-shell)
("Qk" realgud:cmd-kill)
("Sg" realgud:gdb)
("Ss" +realgud:cmd-start)
("q" nil "quit" :color blue) ;; :exit
("Qq" realgud:cmd-quit :color blue)) ;; :exit
(defun +debugger/realgud:gdb-hydra ()
"Run `realgud-hydra'."
(interactive)
(realgud-hydra/body))
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :tools debugger)
:prefix ("d" . "debugger")
:desc "RealGUD hydra" "h" #'+debugger/realgud:gdb-hydra)))
Record and replay rr
We then add some shortcuts to run rr
from Emacs, the rr record
takes the program
name and arguments from my local +realgud-debug-config
, when rr replay
respects
the arguments configured in RealGUD’s GDB command name. Some useful hints could
be found here, here, here and here.
(after! realgud
(defun +debugger/rr-replay ()
"Launch `rr replay'."
(interactive)
(realgud:gdb (+str-replace "gdb" "rr replay" realgud:gdb-command-name)))
(defun +debugger/rr-record ()
"Launch `rr record' with parameters from launch.json or `+launch-json-debug-config'."
(interactive)
(let* ((conf (launch-json--config-choice))
(args (launch-json--substite-special-vars (plist-get conf :program) (plist-get conf :args))))
(unless (make-process :name "rr-record"
:buffer "*rr record*"
:command (append '("rr" "record") args))
(message "Cannot start the 'rr record' process"))))
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :tools debugger)
:prefix ("d" . "debugger")
:desc "rr record" "r" #'+debugger/rr-record
:desc "rr replay" "R" #'+debugger/rr-replay)))
Additional debuggers for RealGUD
(package! realgud-lldb :pin "19a2c0a8b228af543338f3a8e51141a9e23484a5")
(package! realgud-ipdb :pin "f18f907aa4ddd3e59dc19ca296d4ee2dc5e436b0")
(package! realgud-trepan-xpy
:recipe (:host github
:repo "realgud/trepan-xpy")
:pin "f53fea61a86226dcf5222b2814a549c8f8b8d5a9")
(package! realgud-maxima
:recipe (:host github
:repo "realgud/realgud-maxima")
:pin "74d1615be9105d7f8a8d6d0b9f6d7a91638def11")
GDB
Emacs GDB a.k.a. gdb-mi
DAP mode is great, however, it is not mature for C/C++ debugging, it does not
support some basic features like Run until cursor, Show disassembled code, etc.
Emacs have builtin gdb
support through gdb-mi
and gud-gdb
.
The emacs-gdb
package overwrites the builtin gdb-mi
, it is much faster (thanks
to it’s C module), and it defines some easy to use UI, with Visual Studio like
keybindings.
(package! gdb-mi
:disable t
:recipe (:host github
:repo "weirdNox/emacs-gdb"
:files ("*.el" "*.c" "*.h" "Makefile"))
:pin "3313f38ed7d23947992e19f1e464c6d544124144")
(use-package! gdb-mi
:init
(fmakunbound 'gdb)
(fmakunbound 'gdb-enable-debug)
:config
(setq gdb-window-setup-function #'gdb--setup-windows ;; TODO: Customize this
gdb-ignore-gdbinit nil) ;; I use gdbinit to define some useful stuff
;; History
(defvar +gdb-history-file "~/.gdb_history")
(defun +gud-gdb-mode-hook-setup ()
"GDB setup."
;; Suposes "~/.gdbinit" contains:
;; set history save on
;; set history filename ~/.gdb_history
;; set history remove-duplicates 2048
(when (and (ring-empty-p comint-input-ring)
(file-exists-p +gdb-history-file))
(setq comint-input-ring-file-name +gdb-history-file)
(comint-read-input-ring t)))
(add-hook 'gud-gdb-mode-hook '+gud-gdb-mode-hook-setup))
Custom layout for gdb-many-windows
Stolen from https://stackoverflow.com/a/41326527/3058915. I used it to change
the builtin gdb-many-windows
layout.
(setq gdb-many-windows nil)
(defun set-gdb-layout(&optional c-buffer)
(if (not c-buffer)
(setq c-buffer (window-buffer (selected-window)))) ;; save current buffer
;; from http://stackoverflow.com/q/39762833/846686
(set-window-dedicated-p (selected-window) nil) ;; unset dedicate state if needed
(switch-to-buffer gud-comint-buffer)
(delete-other-windows) ;; clean all
(let* ((w-source (selected-window)) ;; left top
(w-gdb (split-window w-source nil 'right)) ;; right bottom
(w-locals (split-window w-gdb nil 'above)) ;; right middle bottom
(w-stack (split-window w-locals nil 'above)) ;; right middle top
(w-breakpoints (split-window w-stack nil 'above)) ;; right top
(w-io (split-window w-source (floor(* 0.9 (window-body-height))) 'below))) ;; left bottom
(set-window-buffer w-io (gdb-get-buffer-create 'gdb-inferior-io))
(set-window-dedicated-p w-io t)
(set-window-buffer w-breakpoints (gdb-get-buffer-create 'gdb-breakpoints-buffer))
(set-window-dedicated-p w-breakpoints t)
(set-window-buffer w-locals (gdb-get-buffer-create 'gdb-locals-buffer))
(set-window-dedicated-p w-locals t)
(set-window-buffer w-stack (gdb-get-buffer-create 'gdb-stack-buffer))
(set-window-dedicated-p w-stack t)
(set-window-buffer w-gdb gud-comint-buffer)
(select-window w-source)
(set-window-buffer w-source c-buffer)))
(defadvice gdb (around args activate)
"Change the way to gdb works."
(setq global-config-editing (current-window-configuration)) ;; to restore: (set-window-configuration c-editing)
(let ((c-buffer (window-buffer (selected-window)))) ;; save current buffer
ad-do-it
(set-gdb-layout c-buffer)))
(defadvice gdb-reset (around args activate)
"Change the way to gdb exit."
ad-do-it
(set-window-configuration global-config-editing))
Highlight current line
(defvar gud-overlay
(let* ((ov (make-overlay (point-min) (point-min))))
(overlay-put ov 'face 'secondary-selection)
ov)
"Overlay variable for GUD highlighting.")
(defadvice gud-display-line (after my-gud-highlight act)
"Highlight current line."
(let* ((ov gud-overlay)
(bf (gud-find-file true-file)))
(with-current-buffer bf
(move-overlay ov (line-beginning-position) (line-beginning-position 2)
;; (move-overlay ov (line-beginning-position) (line-end-position)
(current-buffer)))))
(defun gud-kill-buffer ()
(if (derived-mode-p 'gud-mode)
(delete-overlay gud-overlay)))
(add-hook 'kill-buffer-hook 'gud-kill-buffer)
WIP launch.json
support for GUD and RealGUD
I do a lot of development on C/C++ apps that gets data from command line
arguments, which means I have to type my arguments manually after calling
realgud:gdb
, which is very annoying.
For DAP mode, there is a support for either dap-debug-edit-template
, or
launch.json
. For GUD/RealGUD though, I didn’t find any ready-to-use feature like
this. So let’s code it!
I like to define a variable named +launch-json-debug-config
to use as a
fallback, if no launch.json
file is present.
;; A variable which to be used in .dir-locals.el, formatted as a list of plists;
;; '((:program "..." :args ("args1" "arg2" ...)))
(defvar +launch-json-debug-config nil)
This variable should have the same structure of a launch.json
file, in Elisp, it
should be a list of plists, each plist
represents a configuration, this variable
can be set in .dir-locals.el
for example.
The configuration plists supports some of launch.json
parameters, including:
:name
a description of the debug configuration;:type
which debugger to use;:program
the path of the debuggee program;:args
a list of string arguments to pass to the debuggee program.
The variable +launch-json-debug-config
can be set in a per-project basis thanks
to .dir-locals.el
, something like this:
;; Example entry in .dir-locals.el
((nil . ((+launch-json-debug-config . '((:name "Debug my_prog with csv data"
:type "realgud:gdb" ;; One config with `realgud:gdb'
:program "${workspaceFolder}/build/bin/my_prog"
:args ("--in_file=${workspaceFolder}/some/file.csv"
"--some-parameter" "-a" "-b"
"--out_file=/tmp/some_randome_file"
"-a"))
(:name "Debug my_prog with options '-a' and '-b'"
:type "gdb-mi" ;; Another config with `gdb-mi'
:program "${workspaceFolder}/build/bin/my_prog"
:args ("-a" "-b")))))))
The list of implemented special variables are listed in the table below, they have been defined as specified in VS Code.
Variable | Example |
---|---|
userHome | /home/username |
workspaceFolder | /home/username/your-project |
workspaceFolderBasename | your-project |
file | /home/username/your-project/folder/file.cc |
fileWorkspaceFolder | /home/username/your-project |
relativeFile | folder/file.cc |
relativeFileDirname | folder |
fileBasename | file.cc |
fileBasenameNoExtension | file |
fileDirname | /home/username/your-project/folder |
fileExtname | .cc |
lineNumber | Line number of the cursor |
selectedText | Text selected in your code editor |
pathSeparator | Returns / on *nix, and \ on Windows |
If a launch.json
file is detected in the project directory, it gets read and
searches for a configuration for the realgud:gdb
debugger. So you need to have a
section with type realgud:gdb
. This is an example of a valid launch.json
file.
{
"version": "0.2.0",
"configurations": [
{
"name": "Emacs::RealGUD:GDB (view_trajectory)",
"type": "realgud:gdb",
"request": "launch",
"dap-compilation": "cmake --build build/debug -- -j 8",
"dap-compilation-dir": "${workspaceFolder}",
"program": "${workspaceFolder}/build/debug/bin/view_trajectory",
"args": [
"htraj=${workspaceFolder}/data/seq1/h_poses.csv",
"traj=${workspaceFolder}/data/seq1/poses.csv"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false
}
]
}
The example above defines several parameters, however, only type
, program
and
args
are used at the moment.
(defvar launch-json--gud-debugger-regex
(rx (seq bol (group-n 1 (or "gdb" "gud-gdb" "perldb" "pdb" "jdb" "guiler" "dbx" "sdb" "xdb") eol))))
(defvar launch-json--realgud-debugger-regex
(rx (seq bol (or (seq "realgud:" (group-n 1 (or "gdb" "pdb"
"bashdb" "kshdb" "zshd"
"perldb" "rdebug" "remake"
"trepan" "trepan2" "trepan3k" "trepanjs" "trepan.pl")))
(seq "realgud-" (group-n 1 (or "gub")))
;; Additional debuggers
(seq "realgud:" (group-n 1 (or "xdebug" "pry" "jdb" "ipdb" "trepan-xpy" "trepan-ni" "node-inspect")))
;; `realgud-lldb' defines the debug command as `realgud--lldb',
;; We accept both `realgud:lldb' and `realgud--lldb' in the config
(seq "realgud" (or ":" "--") (group-n 1 (or "lldb")))) eol)))
;; Define aliases for realgud-lldb
(with-eval-after-load 'realgud-lldb
(defalias 'realgud:lldb 'realgud--lldb)
(defalias 'realgud:lldb-command-name 'realgud--lldb-command-name))
;; Define aliases for realgud-ipdb
(with-eval-after-load 'realgud-ipdb
(defalias 'realgud:ipdb-command-name 'realgud--ipdb-command-name))
(defvar launch-json--last-config nil)
(defun launch-json-last-config-clear ()
(interactive)
(setq-local launch-json--last-config nil))
(defun launch-json--substite-special-vars (program &optional args)
"Substitue variables in PROGRAM and ARGS.
Return a list, in which processed PROGRAM is the first element, followed by ARGS."
(let* ((curr-file (ignore-errors (expand-file-name (buffer-file-name))))
(ws-root (string-trim-right
(expand-file-name
(or (projectile-project-root)
(ignore-errors (file-name-directory curr-file))
"."))
"/"))
(ws-basename (file-name-nondirectory ws-root)))
;; Replace special variables
(mapcar
(lambda (str)
(+str-replace-all
(append
(list
(cons "${workspaceFolder}" ws-root)
(cons "${workspaceFolderBasename}" ws-basename)
(cons "${userHome}" (or (getenv "HOME") (expand-file-name "~")))
(cons "${pathSeparator}" (if (memq system-type
'(windows-nt ms-dos cygwin))
"\\" "/"))
(cons "${selectedText}" (if (use-region-p)
(buffer-substring-no-properties
(region-beginning) (region-end)) "")))
;; To avoid problems if launched from a non-file buffer
(when curr-file
(list
(cons "${file}" curr-file)
(cons "${relativeFile}" (file-relative-name curr-file ws-root))
(cons "${relativeFileDirname}" (file-relative-name
(file-name-directory curr-file) ws-root))
(cons "${fileBasename}" (file-name-nondirectory curr-file))
(cons "${fileBasenameNoExtension}" (file-name-base curr-file))
(cons "${fileDirname}" (file-name-directory curr-file))
(cons "${fileExtname}" (file-name-extension curr-file))
(cons "${lineNumber}" (line-number-at-pos (point) t)))))
str))
(cons program args))))
(defun launch-json--debugger-params (type)
(let* ((front/backend
(cond ((string-match launch-json--realgud-debugger-regex type)
(cons 'realgud (intern (match-string 1 type))))
((string-match launch-json--gud-debugger-regex type)
(cons 'gud (intern (match-string 1 type))))
(t
(cons 'unknown 'unknown))))
(frontend (car front/backend))
(backend (cdr front/backend))
(cmd-sym (unless (eq frontend 'unknown)
(intern (format (cond ((eq frontend 'gud) "gud-%s-%s")
((eq frontend 'realgud) "%s-%s")
(t "%s-%s"))
type
"command-name")))))
(message "[launch-json:params]: Found type: %s -> { frontend: %s | backend: %s }"
type (symbol-name frontend) (symbol-name backend))
(cond ((memq backend '(gud-gdb gdb))
;; Special case for '(gud . gdb), uses `gdb-mi'
(let ((use-gdb-mi (equal front/backend '(gud . gdb))))
`(:type ,type
:debug-cmd ,(if use-gdb-mi 'gdb (intern type))
:args-format " --args %s %s"
:cmd ,cmd-sym
:require ,(if use-gdb-mi 'gdb-mi frontend))))
((eq backend 'lldb)
`(:type ,type
:debug-cmd ,(intern type)
:args-format " -- %s %s"
:cmd ,cmd-sym
:require ,(intern (if (eq frontend 'realgud)
(+str-replace-all '(("--" . "-") (":" . "-")) type)
type))))
(t ;; TODO: to be expanded for each debugger
`(:type ,type
:debug-cmd ,(intern type)
:args-format " %s %s"
:cmd ,(if (equal front/backend '(realgud . ipdb)) 'realgud--ipdb-command-name cmd-sym)
:require ,(cond ((equal front/backend '(realgud . trepan-ni)) 'realgud-trepan-ni)
(t frontend)))))))
(defun launch-json--debug-command (params debuggee-args)
"Return the debug command for PARAMS with DEBUGGEE-ARGS."
(when-let* ((prog (car debuggee-args))
(cmd (plist-get params :cmd))
(pkg (plist-get params :require)))
(if (or (not pkg) (eq pkg 'unknown))
(progn (message "[launch-json:command]: Unknown debugger")
nil)
(if (require (plist-get params :require) nil t)
(let ((args (+str-join " " (cdr debuggee-args))))
(when args (setq args (format (plist-get params :args-format) prog args)))
(if (bound-and-true-p cmd)
(concat (eval cmd) (if args args ""))
(message "[launch-json:command]: Invalid command for type %s" (plist-get params :type))
nil))
(message "[launch-json:command]: Cannot add package %s" (symbol-name pkg))
nil))))
(defun launch-json-read (&optional file)
"Return the configurations section from a launch.json FILE.
If FILE is nil, launch.json will be searched in the current project,
if it is set to a launch.json file, it will be used instead."
(let ((launch-json (expand-file-name (or file "launch.json") (or (projectile-project-root) "."))))
(when (file-exists-p launch-json)
(message "[launch-json]: Found \"launch.json\" at %s" launch-json)
(let* ((launch (with-temp-buffer
(insert-file-contents launch-json)
(json-parse-buffer :object-type 'plist :array-type 'list :null-object nil :false-object nil)))
(configs (plist-get launch :configurations)))
(+filter (lambda (conf)
(or (string-match-p launch-json--gud-debugger-regex (plist-get conf :type))
(string-match-p launch-json--realgud-debugger-regex (plist-get conf :type))))
configs)))))
(defun launch-json--config-choice (&optional file)
(let* ((confs (or (launch-json-read file)
+launch-json-debug-config))
(candidates (mapcar (lambda (conf)
(cons (format "%s [%s]" (plist-get conf :name) (plist-get conf :type))
conf))
confs)))
(cond ((eq (length confs) 1)
(car confs))
((> (length confs) 1)
(cdr (assoc (completing-read "Configuration: " candidates) candidates))))))
(defun launch-json-debug (&optional file)
"Launch RealGUD or GDB with parameters from `+launch-json-debug-config' or launch.json file."
(interactive)
(let* ((conf (or launch-json--last-config
(launch-json--config-choice file)))
(args (launch-json--substite-special-vars (plist-get conf :program) (plist-get conf :args)))
(type (plist-get conf :type))
(params (launch-json--debugger-params type)))
(when params
(let ((debug-cmd (plist-get params :debug-cmd)))
(when (fboundp debug-cmd)
(setq-local launch-json--last-config conf)
(funcall debug-cmd
(launch-json--debug-command params args)))))))
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :tools debugger)
:prefix ("d" . "debugger")
:desc "GUD/RealGUD launch.json" "d" #'launch-json-debug))
Valgrind
(package! valgrind
:recipe `(:local-repo ,(expand-file-name "lisp/valgrind" doom-user-dir)))
(use-package! valgrind
:commands valgrind)
Symbols
Emojify
For starters, twitter’s emojis look nicer than emoji-one. Other than that, this is pretty great OOTB 😀.
(setq emojify-emoji-set "twemoji-v2")
One minor annoyance is the use of emojis over the default character when the default is actually preferred. This occurs with overlay symbols I use in Org mode, such as checkbox state, and a few other miscellaneous cases.
We can accommodate our preferences by deleting those entries from the emoji hash table
(defvar emojify-disabled-emojis
'(;; Org
"◼" "☑" "☸" "⚙" "⏩" "⏪" "⬆" "⬇" "❓" "⏱" "®" "™" "🅱" "❌" "✳"
;; Terminal powerline
"✔"
;; Box drawing
"▶" "◀")
"Characters that should never be affected by `emojify-mode'.")
(defadvice! emojify-delete-from-data ()
"Ensure `emojify-disabled-emojis' don't appear in `emojify-emojis'."
:after #'emojify-set-emoji-data
(dolist (emoji emojify-disabled-emojis)
(remhash emoji emojify-emojis)))
Now, it would be good to have a minor mode which allowed you to type ascii/gh emojis and get them converted to unicode. Let’s make one.
(defun emojify--replace-text-with-emoji (orig-fn emoji text buffer start end &optional target)
"Modify `emojify--propertize-text-for-emoji' to replace ascii/github emoticons with unicode emojis, on the fly."
(if (or (not emoticon-to-emoji) (= 1 (length text)))
(funcall orig-fn emoji text buffer start end target)
(delete-region start end)
(insert (ht-get emoji "unicode"))))
(define-minor-mode emoticon-to-emoji
"Write ascii/gh emojis, and have them converted to unicode live."
:global nil
:init-value nil
(if emoticon-to-emoji
(progn
(setq-local emojify-emoji-styles '(ascii github unicode))
(advice-add 'emojify--propertize-text-for-emoji :around #'emojify--replace-text-with-emoji)
(unless emojify-mode
(emojify-turn-on-emojify-mode)))
(setq-local emojify-emoji-styles (default-value 'emojify-emoji-styles))
(advice-remove 'emojify--propertize-text-for-emoji #'emojify--replace-text-with-emoji)))
This new minor mode of ours will be nice for messages, so let’s hook it in for Email and IRC.
(add-hook! '(mu4e-compose-mode org-msg-edit-mode circe-channel-mode) (emoticon-to-emoji 1))
Ligatures
Extra ligatures are good, however, I’d like to see my keywords! Let’s disable them in C/C++, Rust and Python modes. In addition to that, Lisps do replace lambdas with the greek symbol λ, however, this cause miss formatting and sometimes messes up with the parenthesis, so let’s disable ligatures on Lisps.
(defun +appened-to-negation-list (head tail)
(if (sequencep head)
(delete-dups
(if (eq (car tail) 'not)
(append head tail)
(append tail head)))
tail))
(when (modulep! :ui ligatures)
(setq +ligatures-extras-in-modes
(+appened-to-negation-list
+ligatures-extras-in-modes
'(not c-mode c++-mode emacs-lisp-mode python-mode scheme-mode racket-mode rust-mode)))
(setq +ligatures-in-modes
(+appened-to-negation-list
+ligatures-in-modes
'(not emacs-lisp-mode scheme-mode racket-mode))))
Natural languages
Spell-Fu
Install the aspell
back-end and the dictionaries to use with spell-fu
Now, spell-fu
supports multiple languages! Let’s add English, French and Arabic.
So I can “mélanger les langues sans avoir de problèmes!”.
(after! spell-fu
(defun +spell-fu-register-dictionary (lang)
"Add `LANG` to spell-fu multi-dict, with a personal dictionary."
;; Add the dictionary
(spell-fu-dictionary-add (spell-fu-get-ispell-dictionary lang))
(let ((personal-dict-file (expand-file-name (format "aspell.%s.pws" lang) doom-user-dir)))
;; Create an empty personal dictionary if it doesn't exists
(unless (file-exists-p personal-dict-file) (write-region "" nil personal-dict-file))
;; Add the personal dictionary
(spell-fu-dictionary-add (spell-fu-get-personal-dictionary (format "%s-personal" lang) personal-dict-file))))
(add-hook 'spell-fu-mode-hook
(lambda ()
(+spell-fu-register-dictionary +my/lang-main)
(+spell-fu-register-dictionary +my/lang-secondary))))
Proselint
A good and funny linter for English prose!, install via pip install proselint
.
(after! flycheck
(flycheck-define-checker proselint
"A linter for prose."
:command ("proselint" source-inplace)
:error-patterns
((warning line-start (file-name) ":" line ":" column ": "
(id (one-or-more (not (any " "))))
(message) line-end))
:modes (text-mode markdown-mode gfm-mode org-mode))
;; TODO: Can be enabled automatically for English documents using `guess-language'
(defun +flycheck-proselint-toggle ()
"Toggle Proselint checker for the current buffer."
(interactive)
(if (and (fboundp 'guess-language-buffer) (string= "en" (guess-language-buffer)))
(if (memq 'proselint flycheck-checkers)
(setq-local flycheck-checkers (delete 'proselint flycheck-checkers))
(setq-local flycheck-checkers (append flycheck-checkers '(proselint))))
(message "Proselint understands only English!"))))
Grammarly
Use either eglot-grammarly or lsp-grammarly.
(package! grammarly
:recipe (:host github
:repo "emacs-grammarly/grammarly")
:pin "e47b370faace9ca081db0b87ae3bcfd73212c56d")
(use-package! grammarly
:config
(grammarly-load-from-authinfo))
Eglot
(package! eglot-grammarly
:disable (not (modulep! :tools lsp +eglot))
:recipe (:host github
:repo "emacs-grammarly/eglot-grammarly")
:pin "3313f38ed7d23947992e19f1e464c6d544124144")
(use-package! eglot-grammarly
:commands (+lsp-grammarly-load)
:init
(defun +lsp-grammarly-load ()
"Load Grammarly LSP server for Eglot."
(interactive)
(require 'eglot-grammarly)
(call-interactively #'eglot)))
LSP Mode
(package! lsp-grammarly
:disable (or (not (modulep! :tools lsp)) (modulep! :tools lsp +eglot))
:recipe (:host github
:repo "emacs-grammarly/lsp-grammarly")
:pin "eab5292037478c32e7d658fb5cba8b8fb6d72a7c")
(use-package! lsp-grammarly
:commands (+lsp-grammarly-load +lsp-grammarly-toggle)
:init
(defun +lsp-grammarly-load ()
"Load Grammarly LSP server for LSP Mode."
(interactive)
(require 'lsp-grammarly)
(lsp-deferred)) ;; or (lsp)
(defun +lsp-grammarly-enabled-p ()
(not (member 'grammarly-ls lsp-disabled-clients)))
(defun +lsp-grammarly-enable ()
"Enable Grammarly LSP."
(interactive)
(when (not (+lsp-grammarly-enabled-p))
(setq lsp-disabled-clients (remove 'grammarly-ls lsp-disabled-clients))
(message "Enabled grammarly-ls"))
(+lsp-grammarly-load))
(defun +lsp-grammarly-disable ()
"Disable Grammarly LSP."
(interactive)
(when (+lsp-grammarly-enabled-p)
(add-to-list 'lsp-disabled-clients 'grammarly-ls)
(lsp-disconnect)
(message "Disabled grammarly-ls")))
(defun +lsp-grammarly-toggle ()
"Enable/disable Grammarly LSP."
(interactive)
(if (+lsp-grammarly-enabled-p)
(+lsp-grammarly-disable)
(+lsp-grammarly-enable)))
(after! lsp-mode
;; Disable by default
(add-to-list 'lsp-disabled-clients 'grammarly-ls))
:config
(set-lsp-priority! 'grammarly-ls 1))
Grammalecte
(package! flycheck-grammalecte
:recipe (:host github
:repo "milouse/flycheck-grammalecte")
:pin "314de13247710410f11d060a214ac4f400c02a71")
(use-package! flycheck-grammalecte
:when nil ;; BUG: Disabled, there is a Python error
:commands (flycheck-grammalecte-correct-error-at-point
grammalecte-conjugate-verb
grammalecte-define
grammalecte-define-at-point
grammalecte-find-synonyms
grammalecte-find-synonyms-at-point)
:init
(setq grammalecte-settings-file (expand-file-name "grammalecte/grammalecte-cache.el" doom-data-dir)
grammalecte-python-package-directory (expand-file-name "grammalecte/grammalecte" doom-data-dir))
(setq flycheck-grammalecte-report-spellcheck t
flycheck-grammalecte-report-grammar t
flycheck-grammalecte-report-apos nil
flycheck-grammalecte-report-esp nil
flycheck-grammalecte-report-nbsp nil
flycheck-grammalecte-filters
'("(?m)^# ?-*-.+$"
;; Ignore LaTeX equations (inline and block)
"\\$.*?\\$"
"(?s)\\\\begin{\\(?1:\\(?:equation.\\|align.\\)\\)}.*?\\\\end{\\1}"))
(map! :leader :prefix ("l" . "custom")
(:prefix ("g" . "grammalecte")
:desc "Correct error at point" "p" #'flycheck-grammalecte-correct-error-at-point
:desc "Conjugate a verb" "V" #'grammalecte-conjugate-verb
:desc "Define a word" "W" #'grammalecte-define
:desc "Conjugate a verb at point" "w" #'grammalecte-define-at-point
:desc "Find synonyms" "S" #'grammalecte-find-synonyms
:desc "Find synonyms at point" "s" #'grammalecte-find-synonyms-at-point))
:config
(grammalecte-download-grammalecte)
(flycheck-grammalecte-setup))
LTeX/LanguageTool
Originally, LTeX LS stands for LaTeX Language Server, it acts as a Language Server for LaTeX, but not only. It can check the grammar and the spelling of several markup languages such as BibTeX, ConTeXt, LaTeX, Markdown, Org, reStructuredText… and others. Alongside, it provides interfacing with LanguageTool to implement natural language checking.
(after! lsp-ltex
(setq lsp-ltex-language "auto"
lsp-ltex-mother-tongue +my/lang-mother-tongue
flycheck-checker-error-threshold 1000)
(advice-add
'+lsp-ltex-setup :after
(lambda ()
(setq-local lsp-idle-delay 5.0
lsp-progress-function #'lsp-on-progress-legacy
lsp-progress-spinner-type 'half-circle
lsp-ui-sideline-show-code-actions nil
lsp-ui-sideline-show-diagnostics nil
lsp-ui-sideline-enable nil)))
;; FIXME
(defun +lsp-ltex-check-document ()
(interactive)
(when-let ((file (buffer-file-name)))
(let* ((uri (lsp--path-to-uri file))
(beg (region-beginning))
(end (region-end))
(req (if (region-active-p)
`(:uri ,uri
:range ,(lsp--region-to-range beg end))
`(:uri ,uri))))
(lsp-send-execute-command "_ltex.checkDocument" req)))))
Go Translate (Google, Bing and DeepL)
(package! go-translate
:recipe (:host github
:repo "lorniu/go-translate")
:pin "8bbcbce42a7139f079df3e9b9bda0def2cbb690f")
(use-package! go-translate
:commands (gts-do-translate
+gts-yank-translated-region
+gts-translate-with)
:init
;; Your languages pairs
(setq gts-translate-list (list (list +my/lang-main +my/lang-secondary)
(list +my/lang-main +my/lang-mother-tongue)
(list +my/lang-secondary +my/lang-mother-tongue)
(list +my/lang-secondary +my/lang-main)))
(map! :localleader
:map (org-mode-map markdown-mode-map latex-mode-map text-mode-map)
:desc "Yank translated region" "R" #'+gts-yank-translated-region)
(map! :leader :prefix "l"
(:prefix ("G" . "go-translate")
:desc "Bing" "b" (lambda () (interactive) (+gts-translate-with 'bing))
:desc "DeepL" "d" (lambda () (interactive) (+gts-translate-with 'deepl))
:desc "Google" "g" (lambda () (interactive) (+gts-translate-with))
:desc "Yank translated region" "R" #'+gts-yank-translated-region
:desc "gts-do-translate" "t" #'gts-do-translate))
:config
;; Config the default translator, which will be used by the command `gts-do-translate'
(setq gts-default-translator
(gts-translator
;; Used to pick source text, from, to. choose one.
:picker (gts-prompt-picker)
;; One or more engines, provide a parser to give different output.
:engines (gts-google-engine :parser (gts-google-summary-parser))
;; Render, only one, used to consumer the output result.
:render (gts-buffer-render)))
;; Custom texter which remove newlines in the same paragraph
(defclass +gts-translate-paragraph (gts-texter) ())
(cl-defmethod gts-text ((_ +gts-translate-paragraph))
(when (use-region-p)
(let ((text (buffer-substring-no-properties (region-beginning) (region-end))))
(with-temp-buffer
(insert text)
(goto-char (point-min))
(let ((case-fold-search nil))
(while (re-search-forward "\n[^\n]" nil t)
(replace-region-contents
(- (point) 2) (- (point) 1)
(lambda (&optional a b) " ")))
(buffer-string))))))
;; Custom picker to use the paragraph texter
(defclass +gts-paragraph-picker (gts-picker)
((texter :initarg :texter :initform (+gts-translate-paragraph))))
(cl-defmethod gts-pick ((o +gts-paragraph-picker))
(let ((text (gts-text (oref o texter))))
(when (or (null text) (zerop (length text)))
(user-error "Make sure there is any word at point, or selection exists"))
(let ((path (gts-path o text)))
(setq gts-picker-current-path path)
(cl-values text path))))
(defun +gts-yank-translated-region ()
(interactive)
(gts-translate
(gts-translator
:picker (+gts-paragraph-picker)
:engines (gts-google-engine)
:render (gts-kill-ring-render))))
(defun +gts-translate-with (&optional engine)
(interactive)
(gts-translate
(gts-translator
:picker (+gts-paragraph-picker)
:engines
(cond ((eq engine 'deepl)
(gts-deepl-engine
:auth-key ;; Get API key from ~/.authinfo.gpg (machine api-free.deepl.com)
(funcall
(plist-get (car (auth-source-search :host "api-free.deepl.com" :max 1))
:secret))
:pro nil))
((eq engine 'bing) (gts-bing-engine))
(t (gts-google-engine)))
:render (gts-buffer-render)))))
Offline dictionaries
Needs sdcv
to be installed, needs also StarDict dictionaries, you can download
some from here and here for french.
(package! lexic
:recipe (:host github
:repo "tecosaur/lexic")
:pin "f9b3de4d9c2dd1ce5022383e1a504b87bf7d1b09")
(use-package! lexic
:commands (lexic-search lexic-list-dictionary)
:config
(map! :map lexic-mode-map
:n "q" #'lexic-return-from-lexic
:nv "RET" #'lexic-search-word-at-point
:n "a" #'outline-show-all
:n "h" (cmd! (outline-hide-sublevels 3))
:n "o" #'lexic-toggle-entry
:n "n" #'lexic-next-entry
:n "N" (cmd! (lexic-next-entry t))
:n "p" #'lexic-previous-entry
:n "P" (cmd! (lexic-previous-entry t))
:n "E" (cmd! (lexic-return-from-lexic) ; expand
(switch-to-buffer (lexic-get-buffer)))
:n "M" (cmd! (lexic-return-from-lexic) ; minimise
(lexic-goto-lexic))
:n "C-p" #'lexic-search-history-backwards
:n "C-n" #'lexic-search-history-forwards
:n "/" (cmd! (call-interactively #'lexic-search))))
System tools
Disk usage
(package! disk-usage :pin "8792032bb8e7a6ab8a8a9bef89a3964e67bb3cef")
(use-package! disk-usage
:commands (disk-usage))
Chezmoi
(package! chezmoi :pin "781783c483bc8fcdba3a230bb774c3a8a5ebe396")
(use-package! chezmoi
:when CHEZMOI-P
:commands (chezmoi-write
chezmoi-magit-status
chezmoi-diff
chezmoi-ediff
chezmoi-find
chezmoi-write-files
chezmoi-open-other
chezmoi-template-buffer-display
chezmoi-mode)
:config
;; Company integration
(when (modulep! :completion company)
(defun +chezmoi--company-backend-h ()
(require 'chezmoi-company)
(if chezmoi-mode
(add-to-list 'company-backends 'chezmoi-company-backend)
(delete 'chezmoi-company-backend 'company-backends)))
(add-hook 'chezmoi-mode-hook #'+chezmoi--company-backend-h))
;; Integrate with evil mode by toggling template display when entering insert mode.
(when (modulep! :editor evil)
(defun +chezmoi--evil-insert-state-enter-h ()
"Run after evil-insert-state-entry."
(chezmoi-template-buffer-display nil (point))
(remove-hook 'after-change-functions #'chezmoi-template--after-change 1))
(defun +chezmoi--evil-insert-state-exit-h ()
"Run after evil-insert-state-exit."
(chezmoi-template-buffer-display nil)
(chezmoi-template-buffer-display t)
(add-hook 'after-change-functions #'chezmoi-template--after-change nil 1))
(defun +chezmoi--evil-h ()
(if chezmoi-mode
(progn
(add-hook 'evil-insert-state-entry-hook #'+chezmoi--evil-insert-state-enter-h nil 1)
(add-hook 'evil-insert-state-exit-hook #'+chezmoi--evil-insert-state-exit-h nil 1))
(progn
(remove-hook 'evil-insert-state-entry-hook #'+chezmoi--evil-insert-state-enter-h 1)
(remove-hook 'evil-insert-state-exit-hook #'+chezmoi--evil-insert-state-exit-h 1))))
(add-hook 'chezmoi-mode-hook #'+chezmoi--evil-h)))
(map! :leader :prefix ("l" . "custom")
(:prefix ("t" . "tools")
(:when CHEZMOI-P
:prefix ("c" . "chezmoi")
:desc "Magit status" "g" #'chezmoi-magit-status
:desc "Write" "w" #'chezmoi-write
:desc "Write files" "W" #'chezmoi-write-files
:desc "Find source" "f" #'chezmoi-find
:desc "Sync files" "s" #'chezmoi-sync-files
:desc "Diff" "d" #'chezmoi-diff
:desc "EDiff" "e" #'chezmoi-ediff
:desc "Open other" "o" #'chezmoi-open-other)))
Aweshell
(package! aweshell
:recipe (:host github
:repo "manateelazycat/aweshell")
:pin "d246df619573ca3f46070cc0ac82d024271ed243")
(use-package! aweshell
:commands (aweshell-new aweshell-dedicated-open))
Lemon
(package! lemon
:recipe (:host nil
:repo "https://codeberg.org/emacs-weirdware/lemon.git")
:pin "37a6e6d6ef0900ca19c820a2dbc122c7fe6d86cf")
(use-package! lemon
:commands (lemon-mode lemon-display)
:config
(require 'lemon-cpu)
(require 'lemon-memory)
(require 'lemon-network)
(setq lemon-delay 5
lemon-refresh-rate 2
lemon-monitors
(list '((lemon-cpufreq-linux :display-opts '(:sparkline (:type gridded)))
(lemon-cpu-linux)
(lemon-memory-linux)
(lemon-linux-network-tx)
(lemon-linux-network-rx)))))
eCryptfs
(when ECRYPTFS-P
(defvar +ecryptfs-private-dir "Private")
(defvar +ecryptfs-buffer-name "*emacs-ecryptfs*")
(defvar +ecryptfs-config-dir (expand-file-name "~/.ecryptfs"))
(defvar +ecryptfs-passphrase-gpg (expand-file-name "~/.ecryptfs/my-pass.gpg"))
(defvar +ecryptfs--wrapping-independent-p (not (null (expand-file-name "wrapping-independent" +ecryptfs-config-dir))))
(defvar +ecryptfs--wrapped-passphrase-file (expand-file-name "wrapped-passphrase" +ecryptfs-config-dir))
(defvar +ecryptfs--mount-passphrase-sig-file (concat (expand-file-name +ecryptfs-private-dir +ecryptfs-config-dir) ".sig"))
(defvar +ecryptfs--mount-private-cmd "/sbin/mount.ecryptfs_private")
(defvar +ecryptfs--umount-private-cmd "/sbin/umount.ecryptfs_private")
(defvar +ecryptfs--passphrase
(lambda ()
(s-trim-right ;; To remove the new line
(epg-decrypt-file (epg-make-context)
+ecryptfs-passphrase-gpg
nil))))
(defvar +ecryptfs--encrypt-filenames-p
(not (eq 1
(with-temp-buffer
(insert-file-contents +ecryptfs--mount-passphrase-sig-file)
(count-lines (point-min) (point-max))))))
(defvar +ecryptfs--command-format
(if +ecryptfs--encrypt-filenames-p
"ecryptfs-insert-wrapped-passphrase-into-keyring %s '%s'"
"ecryptfs-unwrap-passphrase %s '%s' | ecryptfs-add-passphrase -"))
(defun +ecryptfs-mount-private ()
(interactive)
(unless (and (file-exists-p +ecryptfs--wrapped-passphrase-file)
(file-exists-p +ecryptfs--mount-passphrase-sig-file))
(error "Encrypted private directory \"%s\" is not setup properly."
+ecryptfs-private-dir)
(return))
(let ((try-again t))
(while (and
;; In the first iteration, we try to silently mount the ecryptfs private directory,
;; this would succeed if the key is available in the keyring.
(shell-command +ecryptfs--mount-private-cmd
+ecryptfs-buffer-name)
try-again)
(setq try-again nil)
(message "Encrypted filenames mode [%s]." (if +ecryptfs--encrypt-filenames-p "ENABLED" "DISABLED"))
(shell-command
(format +ecryptfs--command-format
+ecryptfs--wrapped-passphrase-file
(funcall +ecryptfs--passphrase))
+ecryptfs-buffer-name))
(message "Ecryptfs mount private.")))
(defun +ecryptfs-umount-private ()
(interactive)
(while (string-match-p "Sessions still open, not unmounting"
(shell-command-to-string +ecryptfs--umount-private-cmd)))
(message "Unmounted private directory.")))
(map! :leader :prefix ("l" . "custom")
(:prefix ("t" . "tools")
(:when ECRYPTFS-P
:prefix ("e" . "ecryptfs")
:desc "eCryptfs mount private" "e" #'+ecryptfs-mount-private
:desc "eCryptfs un-mount private" "E" #'+ecryptfs-umount-private)))
Features
Workspaces
(map! :leader
(:when (modulep! :ui workspaces)
:prefix ("TAB" . "workspace")
:desc "Display tab bar" "TAB" #'+workspace/display
:desc "Switch workspace" "." #'+workspace/switch-to
:desc "Switch to last workspace" "$" #'+workspace/other ;; Modified
:desc "New workspace" "n" #'+workspace/new
:desc "New named workspace" "N" #'+workspace/new-named
:desc "Load workspace from file" "l" #'+workspace/load
:desc "Save workspace to file" "s" #'+workspace/save
:desc "Delete session" "x" #'+workspace/kill-session
:desc "Delete this workspace" "d" #'+workspace/delete
:desc "Rename workspace" "r" #'+workspace/rename
:desc "Restore last session" "R" #'+workspace/restore-last-session
:desc "Next workspace" ">" #'+workspace/switch-right ;; Modified
:desc "Previous workspace" "<" #'+workspace/switch-left ;; Modified
:desc "Switch to 1st workspace" "1" #'+workspace/switch-to-0
:desc "Switch to 2nd workspace" "2" #'+workspace/switch-to-1
:desc "Switch to 3rd workspace" "3" #'+workspace/switch-to-2
:desc "Switch to 4th workspace" "4" #'+workspace/switch-to-3
:desc "Switch to 5th workspace" "5" #'+workspace/switch-to-4
:desc "Switch to 6th workspace" "6" #'+workspace/switch-to-5
:desc "Switch to 7th workspace" "7" #'+workspace/switch-to-6
:desc "Switch to 8th workspace" "8" #'+workspace/switch-to-7
:desc "Switch to 9th workspace" "9" #'+workspace/switch-to-8
:desc "Switch to final workspace" "0" #'+workspace/switch-to-final))
Weather
(package! wttrin
:recipe `(:local-repo ,(expand-file-name "lisp/wttrin" doom-user-dir))
:pin "df5427ce2a5ad4dab652dbb1c4a1834d7ddc2abc")
;; https://raw.githubusercontent.com/tecosaur/emacs-config/master/lisp/wttrin/wttrin.el
(use-package! wttrin
:commands wttrin)
OpenStreetMap
(package! osm :pin "808baabecd9882736b240e6ea9344047aeb669e2")
(use-package! osm
:commands (osm-home
osm-search
osm-server
osm-goto
osm-gpx-show
osm-bookmark-jump)
:custom
;; Take a look at the customization group `osm' for more options.
(osm-server 'default) ;; Configure the tile server
(osm-copyright t) ;; Display the copyright information
:init
(setq osm-tile-directory (expand-file-name "osm" doom-data-dir))
;; Load Org link support
(with-eval-after-load 'org
(require 'osm-ol)))
Islamic prayer times
(package! awqat
:recipe (:host github
:repo "zkry/awqat")
:pin "72b821aad0cb16165e27643c7d968e1528f00f8d")
(use-package! awqat
:commands (awqat-display-prayer-time-mode awqat-times-for-day)
:config
;; Make sure `calendar-latitude' and `calendar-longitude' are set,
;; otherwise, set them here.
(setq awqat-asr-hanafi nil
awqat-mode-line-format " 🕌 ${prayer} (${hours}h${minutes}m) ")
(awqat-set-preset-french-muslims))
Info colors
Better colors for manual pages.
(package! info-colors :pin "47ee73cc19b1049eef32c9f3e264ea7ef2aaf8a5")
(use-package! info-colors
:commands (info-colors-fontify-node))
(add-hook 'Info-selection-hook 'info-colors-fontify-node)
Zotero Zotxt
(package! zotxt :pin "96a132d6b39f6bc19a58913b761d42efc198f8a4")
(use-package! zotxt
:when ZOTERO-P
:commands org-zotxt-mode)
CRDT
Collaborative editing for geeks! crdt.el
adds support for Conflict-free
Replicated Data Type.
(package! crdt :pin "ec0b9cc652c0e980d5865abbba7cbffefea6e8cc")
(use-package! crdt
:commands (crdt-share-buffer
crdt-connect)
:init
(cond
(TUNTOX-P (setq crdt-use-tuntox t
crdt-tuntox-password-in-url t))
(STUNNEL-P (setq crdt-use-stunnel t))))
The Silver Searcher
An Emacs front-end to The Silver Searcher, first we need to install ag
using sudo pacman -S the_silver_searcher
.
(package! ag :pin "ed7e32064f92f1315cecbfc43f120bbc7508672c")
(use-package! ag
:when AG-P
:commands (ag
ag-files
ag-regexp
ag-project
ag-project-files
ag-project-regexp))
Page break lines
A feature that displays ugly form feed characters as tidy horizontal rules. Inspired by M-EMACS.
(package! page-break-lines :pin "79eca86e0634ac68af862e15c8a236c37f446dcd")
(use-package! page-break-lines
:diminish
:init (global-page-break-lines-mode))
Emacs Application Framework
EAF is presented as: A free/libre and open-source extensible framework that revolutionizes the graphical capabilities of Emacs. Or the key to ultimately Live in Emacs.
First, install EAF as specified in the project’s readme. To update EAF, we need
to run git pull ; ./install-eaf.py
in lisp/emacs-application-framework
and (M-x eaf-install-and-update)
in Emacs. This updates EAF, applications and their
dependencies.
(use-package! eaf
:when EAF-P
:load-path EAF-DIR
:commands (eaf-open
eaf-open-browser
eaf-open-jupyter
+eaf-open-mail-as-html)
:init
(defvar +eaf-enabled-apps
'(org browser mindmap jupyter org-previewer markdown-previewer file-sender video-player))
(defun +eaf-app-p (app-symbol)
(memq app-symbol +eaf-enabled-apps))
(when (+eaf-app-p 'browser)
;; Make EAF Browser my default browser
(setq browse-url-browser-function #'eaf-open-browser)
(defalias 'browse-web #'eaf-open-browser)
(map! :localleader
:map (mu4e-headers-mode-map mu4e-view-mode-map)
:desc "Open mail as HTML" "h" #'+eaf-open-mail-as-html
:desc "Open URL (EAF)" "o" #'eaf-open-browser))
(when (+eaf-app-p 'pdf-viewer)
(after! org
;; Use EAF PDF Viewer in Org
(defun +eaf--org-open-file-fn (file &optional link)
"An wrapper function on `eaf-open'."
(eaf-open file))
;; use `emacs-application-framework' to open PDF file: link
(add-to-list 'org-file-apps '("\\.pdf\\'" . +eaf--org-open-file-fn)))
(after! latex
;; Link EAF with the LaTeX compiler in emacs. When a .tex file is open,
;; the Command>Compile and view (C-c C-a) option will compile the .tex
;; file into a .pdf file and display it using EAF. Double clicking on the
;; PDF side jumps to editing the clicked section.
(add-to-list 'TeX-command-list '("XeLaTeX" "%`xelatex --synctex=1%(mode)%' %t" TeX-run-TeX nil t))
(add-to-list 'TeX-view-program-list '("eaf" eaf-pdf-synctex-forward-view))
(add-to-list 'TeX-view-program-selection '(output-pdf "eaf"))))
:config
;; Generic
(setq eaf-start-python-process-when-require t
eaf-kill-process-after-last-buffer-closed t
eaf-fullscreen-p nil)
;; Debug
(setq eaf-enable-debug nil)
;; Web engine
(setq eaf-webengine-font-family (symbol-name (font-get doom-font :family))
eaf-webengine-fixed-font-family (symbol-name (font-get doom-font :family))
eaf-webengine-serif-font-family (symbol-name (font-get doom-serif-font :family))
eaf-webengine-font-size 16
eaf-webengine-fixed-font-size 16
eaf-webengine-enable-scrollbar t
eaf-webengine-scroll-step 200
eaf-webengine-default-zoom 1.25
eaf-webengine-show-hover-link t
eaf-webengine-download-path "~/Downloads"
eaf-webengine-enable-plugin t
eaf-webengine-enable-javascript t
eaf-webengine-enable-javascript-access-clipboard t)
(when (display-graphic-p)
(require 'eaf-all-the-icons)
(mapc (lambda (v) (eaf-all-the-icons-icon (car v)))
eaf-all-the-icons-alist))
;; Browser settings
(when (+eaf-app-p 'browser)
(setq eaf-browser-continue-where-left-off t
eaf-browser-dark-mode nil ;; "follow"
eaf-browser-enable-adblocker t
eaf-browser-enable-autofill nil
eaf-browser-remember-history t
eaf-browser-ignore-history-list '("google.com/search" "file://")
eaf-browser-text-selection-color "auto"
eaf-browser-translate-language +my/lang-main
eaf-browser-blank-page-url "https://www.duckduckgo.com"
eaf-browser-chrome-history-file "~/.config/google-chrome/Default/History"
eaf-browser-default-search-engine "duckduckgo"
eaf-browser-continue-where-left-off t
eaf-browser-aria2-auto-file-renaming t)
(require 'eaf-browser)
(defun +eaf-open-mail-as-html ()
"Open the html mail in EAF Browser."
(interactive)
(let ((msg (mu4e-message-at-point t))
;; Bind browse-url-browser-function locally, so it works
;; even if EAF Browser is not set as a default browser.
(browse-url-browser-function #'eaf-open-browser))
(if msg
(mu4e-action-view-in-browser msg)
(message "No message at point.")))))
;; File manager settings
(when (+eaf-app-p 'file-manager)
(setq eaf-file-manager-show-preview nil
eaf-find-alternate-file-in-dired t
eaf-file-manager-show-hidden-file t
eaf-file-manager-show-icon t)
(require 'eaf-file-manager))
;; File Browser
(when (+eaf-app-p 'file-browser)
(require 'eaf-file-browser))
;; PDF Viewer settings
(when (+eaf-app-p 'pdf-viewer)
(setq eaf-pdf-dark-mode "follow"
eaf-pdf-show-progress-on-page nil
eaf-pdf-dark-exclude-image t
eaf-pdf-notify-file-changed t)
(require 'eaf-pdf-viewer))
;; Org
(when (+eaf-app-p 'rss-reader)
(setq eaf-rss-reader-split-horizontally nil
eaf-rss-reader-web-page-other-window t)
(require 'eaf-org))
;; Org
(when (+eaf-app-p 'org)
(require 'eaf-org))
;; Mail
;; BUG The `eaf-open-mail-as-html' is not working,
;; I use `+eaf-open-mail-as-html' instead
(when (+eaf-app-p 'mail)
(require 'eaf-mail))
;; Org Previewer
(when (+eaf-app-p 'org-previewer)
(setq eaf-org-dark-mode "follow")
(require 'eaf-org-previewer))
;; Markdown Previewer
(when (+eaf-app-p 'markdown-previewer)
(setq eaf-markdown-dark-mode "follow")
(require 'eaf-markdown-previewer))
;; Jupyter
(when (+eaf-app-p 'jupyter)
(setq eaf-jupyter-dark-mode "follow"
eaf-jupyter-font-family (symbol-name (font-get doom-font :family))
eaf-jupyter-font-size 13)
(require 'eaf-jupyter))
;; Mindmap
(when (+eaf-app-p 'mindmap)
(setq eaf-mindmap-dark-mode "follow"
eaf-mindmap-save-path "~/Dropbox/Mindmap")
(require 'eaf-mindmap))
;; File Sender
(when (+eaf-app-p 'file-sender)
(require 'eaf-file-sender))
;; Music Player
(when (+eaf-app-p 'music-player)
(require 'eaf-music-player))
;; Video Player
(when (+eaf-app-p 'video-player)
(setq eaf-video-player-keybinding
'(("p" . "toggle_play")
("q" . "close_buffer")
("h" . "play_backward")
("l" . "play_forward")
("j" . "decrease_volume")
("k" . "increase_volume")
("f" . "toggle_fullscreen")
("R" . "restart")))
(require 'eaf-video-player))
;; Image Viewer
(when (+eaf-app-p 'image-viewer)
(require 'eaf-image-viewer))
;; Git
(when (+eaf-app-p 'git)
(require 'eaf-git))
;; Fix EVIL keybindings
(after! evil
(require 'eaf-evil)
(define-key key-translation-map (kbd "SPC")
(lambda (prompt)
(if (derived-mode-p 'eaf-mode)
(pcase eaf--buffer-app-name
("browser" (if (eaf-call-sync "execute_function" eaf--buffer-id "is_focus")
(kbd "SPC")
(kbd eaf-evil-leader-key)))
("pdf-viewer" (kbd eaf-evil-leader-key))
("image-viewer" (kbd eaf-evil-leader-key))
("music-player" (kbd eaf-evil-leader-key))
("video-player" (kbd eaf-evil-leader-key))
("file-sender" (kbd eaf-evil-leader-key))
("mindmap" (kbd eaf-evil-leader-key))
(_ (kbd "SPC")))
(kbd "SPC"))))))
Bitwarden
(package! bitwarden
:recipe (:host github
:repo "seanfarley/emacs-bitwarden")
:pin "02d6410003a42e7fbeb4aa109aba949eea553706")
(use-package! bitwarden
;;:config
;;(bitwarden-auth-source-enable)
:when BITWARDEN-P
:init
(setq bitwarden-automatic-unlock
(lambda ()
(require 'auth-source)
(if-let* ((matches (auth-source-search :host "bitwarden.com" :max 1))
(entry (nth 0 matches))
(email (plist-get entry :user))
(pass (plist-get entry :secret)))
(progn
(setq bitwarden-user email)
(if (functionp pass) (funcall pass) pass))
""))))
PDF tools
The pdf-tools
package supports dark mode (midnight), I use Emacs often to write
and read PDF documents, so let’s make it dark by default, this can be toggled
using the m z
.
(after! pdf-tools
;; Auto install
(pdf-tools-install-noverify)
(setq-default pdf-view-image-relief 2
pdf-view-display-size 'fit-page)
(add-hook! 'pdf-view-mode-hook
(when (memq doom-theme '(modus-vivendi doom-one doom-dark+ doom-vibrant))
;; TODO: find a more generic way to detect if we are in a dark theme
(pdf-view-midnight-minor-mode 1)))
;; Color the background, so we can see the PDF page borders
;; https://protesilaos.com/emacs/modus-themes#h:ff69dfe1-29c0-447a-915c-b5ff7c5509cd
(defun +pdf-tools-backdrop ()
(face-remap-add-relative
'default
`(:background ,(if (memq doom-theme '(modus-vivendi modus-operandi))
(modus-themes-color 'bg-alt)
(doom-color 'bg-alt)))))
(add-hook 'pdf-tools-enabled-hook #'+pdf-tools-backdrop))
(after! pdf-links
;; Tweak for Modus and `pdf-links'
(when (memq doom-theme '(modus-vivendi modus-operandi))
;; https://protesilaos.com/emacs/modus-themes#h:2659d13e-b1a5-416c-9a89-7c3ce3a76574
(let ((spec (apply #'append
(mapcar
(lambda (name)
(list name
(face-attribute 'pdf-links-read-link
name nil 'default)))
'(:family :width :weight :slant)))))
(setq pdf-links-read-link-convert-commands
`("-density" "96"
"-family" ,(plist-get spec :family)
"-stretch" ,(let* ((width (plist-get spec :width))
(name (symbol-name width)))
(replace-regexp-in-string "-" ""
(capitalize name)))
"-weight" ,(pcase (plist-get spec :weight)
('ultra-light "Thin")
('extra-light "ExtraLight")
('light "Light")
('semi-bold "SemiBold")
('bold "Bold")
('extra-bold "ExtraBold")
('ultra-bold "Black")
(_weight "Normal"))
"-style" ,(pcase (plist-get spec :slant)
('italic "Italic")
('oblique "Oblique")
(_slant "Normal"))
"-pointsize" "%P"
"-undercolor" "%f"
"-fill" "%b"
"-draw" "text %X,%Y '%c'")))))
LTDR
Add the tldr.el
client for TLDR pages.
(package! tldr :pin "d3fd2a809a266c005915026799121c78e8b358f0")
(use-package! tldr
:commands (tldr-update-docs tldr)
:init
(setq tldr-enabled-categories '("common" "linux" "osx" "sunos")))
FZF
(package! fzf :pin "21912ebc7e1084aa88c9d8b7715e782a3978ed23")
(after! evil
(evil-define-key 'insert fzf-mode-map (kbd "ESC") #'term-kill-subjob))
(define-minor-mode fzf-mode
"Minor mode for the FZF buffer"
:init-value nil
:lighter " FZF"
:keymap '(("C-c" . term-kill-subjob)))
(defadvice! doom-fzf--override-start-args-a (original-fn &rest args)
"Set the FZF minor mode with the fzf buffer."
:around #'fzf/start
(message "called with args %S" args)
(apply original-fn args)
;; set the FZF buffer to fzf-mode so we can hook ctrl+c
(set-buffer "*fzf*")
(fzf-mode))
(defvar fzf/args
"-x --print-query -m --tiebreak=index --expect=ctrl-v,ctrl-x,ctrl-t")
(use-package! fzf
:commands (fzf fzf-projectile fzf-hg fzf-git fzf-git-files fzf-directory fzf-git-grep))
Fun
Speed Type
A game to practice speed typing in Emacs.
(package! speed-type :pin "304cb8cd6c30d07577d7d864fd32858a29a73dba")
(use-package! speed-type
:commands (speed-type-text))
2048 Game
(package! 2048-game :pin "87936ac2ff37f92daf7a69ae86ba226b031349e4")
(use-package! 2048-game
:commands (2048-game))
Snow
Let it snow in Emacs!
(package! snow :pin "4cd41a703b730a6b59827853f06b98d91405df5a")
(use-package! snow
:commands (snow))
xkcd
(package! xkcd
:recipe (:host github
:repo "vibhavp/emacs-xkcd")
:pin "80011da2e7def8f65233d4e0d790ca60d287081d")
(use-package! xkcd
:commands (xkcd-get xkcd)
:config
(setq xkcd-cache-dir (expand-file-name "xkcd/" doom-cache-dir)
xkcd-cache-latest (expand-file-name "xkcd/latest" doom-cache-dir)))
Applications
Calendar
(setq calendar-latitude 48.7
calendar-longitude 2.17
calendar-location-name "Orsay, FR"
calendar-time-display-form
'(24-hours ":" minutes
(if time-zone " (") time-zone (if time-zone ")")))
e-Books (nov
)
(package! nov :pin "cb5f45cbcfbcf263cdeb2d263eb15edefc8b07cb")
Use nov
to read EPUB e-books.
(use-package! nov
:mode ("\\.epub\\'" . nov-mode)
:config
(map! :map nov-mode-map
:n "RET" #'nov-scroll-up)
(defun doom-modeline-segment--nov-info ()
(concat " "
(propertize (cdr (assoc 'creator nov-metadata))
'face 'doom-modeline-project-parent-dir)
" "
(cdr (assoc 'title nov-metadata))
" "
(propertize (format "%d/%d" (1+ nov-documents-index) (length nov-documents))
'face 'doom-modeline-info)))
(advice-add 'nov-render-title :override #'ignore)
(defun +nov-mode-setup ()
(face-remap-add-relative 'variable-pitch
:family "Merriweather"
:height 1.4
:width 'semi-expanded)
(face-remap-add-relative 'default :height 1.3)
(setq-local line-spacing 0.2
next-screen-context-lines 4
shr-use-colors nil)
(require 'visual-fill-column nil t)
(setq-local visual-fill-column-center-text t
visual-fill-column-width 80
nov-text-width 80)
(visual-fill-column-mode 1)
(hl-line-mode -1)
(add-to-list '+lookup-definition-functions
#'+lookup/dictionary-definition)
(setq-local mode-line-format
`((:eval
(doom-modeline-segment--workspace-name))
(:eval
(doom-modeline-segment--window-number))
(:eval
(doom-modeline-segment--nov-info))
,(propertize
" %P "
'face 'doom-modeline-buffer-minor-mode)
,(propertize
" "
'face (if (doom-modeline--active) 'mode-line 'mode-line-inactive)
'display `((space
:align-to
(- (+ right right-fringe right-margin)
,(* (let ((width (doom-modeline--font-width)))
(or (and (= width 1) 1)
(/ width (frame-char-width) 1.0)))
(string-width
(format-mode-line (cons "" '(:eval (doom-modeline-segment--major-mode))))))))))
(:eval (doom-modeline-segment--major-mode)))))
(add-hook 'nov-mode-hook #'+nov-mode-setup))
News feed (elfeed
)
Set RSS news feeds
(setq elfeed-feeds
'("https://arxiv.org/rss/cs.RO"
"https://interstices.info/feed"
"https://this-week-in-rust.org/rss.xml"
"https://planet.emacslife.com/atom.xml"
"https://www.omgubuntu.co.uk/feed"
"https://itsfoss.com/feed"
"https://linuxhandbook.com/feed"
"https://spectrum.ieee.org/rss/robotics/fulltext"
"https://spectrum.ieee.org/rss/aerospace/fulltext"
"https://spectrum.ieee.org/rss/computing/fulltext"
"https://spectrum.ieee.org/rss/blog/automaton/fulltext"
"https://developers.redhat.com/blog/feed"
"https://lwn.net/headlines/rss"))
VPN configuration
NetExtender wrapper
I store my NetExtender VPN parameters in a GPG encrypted file. The credentials file
contains a line of private parameters to pass to netExtender
, like this:
echo "-u <USERNAME> -d <DOMAINE> -p <PASSWORD> -s <SERVER_IP>" \
| gpg -c > sslvpn.gpg
Then I like to have a simple script which decrypt the credentials and launch a
session via the netExtender
command.
#!/bin/bash
if ! command -v netExtender &> /dev/null
then
echo "netExtender not found, installing from AUR using 'yay'"
yay -S netextender
fi
MY_LOGIN_PARAMS_FILE="$HOME/.ssh/sslvpn.gpg"
echo "Y\n" | netExtender --auto-reconnect \
$(gpg -q --for-your-eyes-only --no-tty -d "${MY_LOGIN_PARAMS_FILE}")
Emacs + NetExtender
(when NETEXTENDER-P
(defvar +netextender-process-name "netextender")
(defvar +netextender-buffer-name " *NetExtender*")
(defvar +netextender-command '("~/.local/bin/netextender"))
(defun +netextender-start ()
"Launch a NetExtender VPN session"
(interactive)
(unless (get-process +netextender-process-name)
(if (make-process :name +netextender-process-name
:buffer +netextender-buffer-name
:command +netextender-command)
(message "Started NetExtender VPN session")
(message "Cannot start NetExtender"))))
(defun +netextender-kill ()
"Kill the created NetExtender VPN session"
(interactive)
(when (get-process +netextender-process-name)
(if (kill-buffer +netextender-buffer-name)
(message "Killed NetExtender VPN session")
(message "Cannot kill NetExtender"))))
(map! :leader
:prefix ("l")
(:prefix ("t")
(:prefix ("n" . "netExtender")
:desc "Start" "s" #'+netextender-start
:desc "Kill" "k" #'+netextender-kill))))
Email (mu4e
)
Configuring mu4e
as email client needs three parts:
- Incoming mail configuration IMAP (using
mbsync
) - Outgoing mail configuration SMTP (using
smtpmail
ormsmtp
) - Email indexer and viewer (via
mu
andmu4e
)
IMAP (mbsync
)
You will need to:
- Install
mu
andisync
(sudo pacman -S mu isync
) - Set up a proper configuration file for your accounts at
~/.mbsyncrc
- Run
mu init --maildir=~/Maildir --my-address=user@host1 --my-address=user@host2
- Run
mbsync -c ~/.mbsyncrc -a
- For sending mails from
mu4e
, add a~/.authinfo
file, file contains a line in this formatmachine MAIL.DOMAIN.TLD login USER port 587 password PASSWD
- Encrypt the
~/.authinfo
file using GPGgpg -c ~/.authinfo
and delete the original unencrypted file.
I use a mbsyncrc
file for multi-accounts, with some hacks for Gmail accounts (to
rename the [Gmail]/...
folders). Here is an explained configuration example.
In the configuration file, there is a parameter named Pass
which should be set
to the password in plain text. Most of the examples you can find online uses
this parameter, but in real life, nobody uses it, it is extremely unsafe to put
the password in plain text configuration file. Instead, mbsync
configuration
file provides the alternative PassCmd
parameter, which can be set to an
arbitrary shell command which gets the password for you. You can set it for
example to call the pass
password manager to output the account password, or to
bw
command (for Bitwarden users). For me, I’m using it with Emacs'
~/.authinfo.gpg
, the PassCmd
in my configuration uses GPG and awk
to decrypt and
filter the file content to find the required account’s password. I set PassCmd
to something like this:
gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp\.googlemail\.com login username@gmail\.com/ {print $NF}'
Remember the line format in the ~/.authinfo.gpg
file:
machine smtp.googlemail.com login username@gmail.com port 587 password PASSWD
This PassCmd
command above, decrypts the ~/.authinfo.gpg
, passes it to awk
to
search the line containing "machine smtp.googlemail.com login username@gmail.com"
and prints the last field (the last field $NF
in the awk
command corresponds to the password, as you can see in the line format).
The whole ~/.mbsync
file should look like this:
# mbsync config file
# GLOBAL OPTIONS
BufferLimit 50mb # Global option: Default buffer size is 10M, too small for modern machines.
Sync All # Channels global: Sync everything "Pull Push New ReNew Delete Flags" (default option)
Create Both # Channels global: Automatically create missing mailboxes on both sides
Expunge Both # Channels global: Delete messages marked for deletion on both sides
CopyArrivalDate yes # Channels global: Propagate arrival time with the messages
# SECTION (IMAP4 Accounts)
IMAPAccount work # IMAP Account name
Host mail.host.ccc # The host to connect to
User user@host.ccc # Login user name
SSLVersions TLSv1.2 TLSv1.1 # Supported SSL versions
# Extract password from encrypted ~/.authinfo.gpg
# File format: "machine <SERVER> login <LOGIN> port <PORT> password <PASSWORD>"
# This uses sed to extract <PASSWORD> from line matching the account's <SERVER>
PassCmd "gpg2 -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp.domain.tld/ {print $NF}'"
AuthMechs * # Authentication mechanisms
SSLType IMAPS # Protocol (STARTTLS/IMAPS)
CertificateFile /etc/ssl/certs/ca-certificates.crt
# END OF SECTION
# IMPORTANT NOTE: you need to keep the blank line after each section
# SECTION (IMAP Stores)
IMAPStore work-remote # Remote storage name
Account work # Associated account
# END OF SECTION
# SECTION (Maildir Stores)
MaildirStore work-local # Local storage (create directories with mkdir -p ~/Maildir/<ACCOUNT-NAME>)
Path ~/Maildir/work/ # The local store path
Inbox ~/Maildir/work/Inbox # Location of the INBOX
SubFolders Verbatim # Download all sub-folders
# END OF SECTION
# Connections specify links between remote and local folders
# they are specified using patterns, which match remote mail
# folders. Some commonly used patters include:
#
# - "*" to match everything
# - "!DIR" to exclude "DIR"
# - "DIR" to match DIR
#
# SECTION (Channels)
Channel work # Channel name
Far :work-remote: # Connect remote store
Near :work-local: # to the local one
Patterns "INBOX" "Drafts" "Sent" "Archives/*" "Spam" "Trash"
SyncState * # Save state in near side mailbox file ".mbsyncstate"
# END OF SECTION
# =================================================================================
IMAPAccount gmail
Host imap.gmail.com
User user@gmail.com
PassCmd "gpg2 -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp.domain.tld/ {print $NF}'"
AuthMechs LOGIN
SSLType IMAPS
CertificateFile /etc/ssl/certs/ca-certificates.crt
IMAPStore gmail-remote
Account gmail
MaildirStore gmail-local
Path ~/Maildir/gmail/
Inbox ~/Maildir/gmail/Inbox
# For Gmail, I like to make multiple channels, one for each remote directory
# this is a trick to rename remote "[Gmail]/mailbox" to "mailbox"
Channel gmail-inbox
Far :gmail-remote:
Near :gmail-local:
Patterns "INBOX"
SyncState *
Channel gmail-trash
Far :gmail-remote:"[Gmail]/Trash"
Near :gmail-local:"Trash"
SyncState *
Channel gmail-drafts
Far :gmail-remote:"[Gmail]/Drafts"
Near :gmail-local:"Drafts"
SyncState *
Channel gmail-sent
Far :gmail-remote:"[Gmail]/Sent Mail"
Near :gmail-local:"Sent Mail"
SyncState *
Channel gmail-all
Far :gmail-remote:"[Gmail]/All Mail"
Near :gmail-local:"All Mail"
SyncState *
Channel gmail-starred
Far :gmail-remote:"[Gmail]/Starred"
Near :gmail-local:"Starred"
SyncState *
Channel gmail-spam
Far :gmail-remote:"[Gmail]/Spam"
Near :gmail-local:"Spam"
SyncState *
# GROUPS PUT TOGETHER CHANNELS, SO THAT WE CAN INVOKE
# MBSYNC ON A GROUP TO SYNC ALL CHANNELS
#
# FOR INSTANCE: "mbsync gmail" GETS MAIL FROM
# "gmail-inbox", "gmail-sent", and "gmail-trash"
#
# SECTION (Groups)
Group gmail
Channel gmail-inbox
Channel gmail-sent
Channel gmail-trash
Channel gmail-drafts
Channel gmail-all
Channel gmail-starred
Channel gmail-spam
# END OF SECTION
SMTP (msmtp
)
I was using the standard smtpmail
to send mails; but recently, I’m getting
problems when sending mails. I passed a whole day trying to fix mail sending for
one of my accounts, at the end of the day, I got a working setup; BUT, sending
the first mail always ask me about password! I need to enter the password to be
able to send the mail, Emacs asks me then if I want to save it to
~/.authifo.gpg
, when I confirm saving it, it got duplicated in the .authinfo.gpg
file.
This seems to be a bug; I also found somewhere that smtpmail
is buggy, and that
msmtp
seems to be a good alternative, so now I’m using a msmtp
-based setup, and
it works like a charm!
For this, we will need an additional configuration file, ~/.msmtprc
, I configure
it the same way as mbsync
, specifying this time SMTP servers instead of IMAP
ones. I extract the passwords from ~/.authinfo.gpg
using GPG and awk
, the same
way we did in mbsync
’s configuration.
The following is a sample file ~/.msmtprc
.
# Set default values for all following accounts.
defaults
auth on
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.msmtp.log
# Gmail
account gmail
auth plain
host smtp.googlemail.com
port 587
from username@gmail.com
user username
passwordeval "gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp.googlemail.com login .*@gmail.com/ {print $NF}'"
add_missing_date_header on
## Gmail - aliases
account alias-account : gmail
from alias@mail.com
account other-alias : gmail
from other.alias@address.org
# Work
account work
auth on
host smtp.domaine.tld
port 587
from username@domaine.tld
user username
passwordeval "gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp.domaine.tld/ {print $NF}'"
tls_nocertcheck # ignore TLS certificate errors
Mail client and indexer (mu
and mu4e
)
Add mu4e
to path if it exists on the file system.
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
I configure my email accounts in a private file in
lisp/private/+mu4e-accounts.el
, which will be loaded after this common part:
(after! mu4e
(require 'mu4e-contrib)
(require 'mu4e-icalendar)
(require 'org-agenda)
;; Common parameters
(setq mu4e-update-interval (* 3 60) ;; Every 3 min
mu4e-index-update-error-warning nil ;; Do not show warning after update
mu4e-get-mail-command "mbsync -a" ;; Not needed, as +mu4e-backend is 'mbsync by default
mu4e-main-hide-personal-addresses t ;; No need to display a long list of my own addresses!
mu4e-attachment-dir (expand-file-name "~/Downloads/mu4e-attachements")
mu4e-sent-messages-behavior 'sent ;; Save sent messages
mu4e-context-policy 'pick-first ;; Start with the first context
mu4e-compose-context-policy 'ask) ;; Always ask which context to use when composing a new mail
;; Use msmtp instead of smtpmail
(setq sendmail-program (executable-find "msmtp")
send-mail-function #'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from")
message-send-mail-function #'message-send-mail-with-sendmail
message-sendmail-envelope-from 'obey-mail-envelope-from
mail-envelope-from 'header
mail-personal-alias-file (expand-file-name "mail-aliases.mailrc" doom-user-dir)
mail-specify-envelope-from t)
(setq mu4e-headers-fields '((:flags . 6) ;; 3 flags
(:account-stripe . 2)
(:from-or-to . 25)
(:folder . 10)
(:recipnum . 2)
(:subject . 80)
(:human-date . 8))
+mu4e-min-header-frame-width 142
mu4e-headers-date-format "%d/%m/%y"
mu4e-headers-time-format "⧖ %H:%M"
mu4e-search-results-limit 1000
mu4e-index-cleanup t)
(defvar +mu4e-header--folder-colors nil)
(appendq! mu4e-header-info-custom
'((:folder .
(:name "Folder" :shortname "Folder" :help "Lowest level folder" :function
(lambda (msg)
(+mu4e-colorize-str
(replace-regexp-in-string "\\`.*/" "" (mu4e-message-field msg :maildir))
'+mu4e-header--folder-colors))))))
;; Add a unified inbox shortcut
(add-to-list
'mu4e-bookmarks
'(:name "Unified inbox" :query "maildir:/.*inbox/" :key ?i) t)
;; Add shortcut to view yesterday's messages
(add-to-list
'mu4e-bookmarks
'(:name "Yesterday's messages" :query "date:1d..today" :key ?y) t)
;; Load a list of my email addresses '+my-addresses', defined as:
;; (setq +my-addresses '("user@gmail.com" "user@hotmail.com"))
(load! "lisp/private/+my-addresses.el")
(when (bound-and-true-p +my-addresses)
;; I like always to add myself in BCC, Lets add a bookmark to show all my BCC mails
(defun +mu-long-query (query oper arg-list)
(concat "(" (+str-join (concat " " oper " ") (mapcar (lambda (addr) (format "%s:%s" query addr)) arg-list)) ")"))
;; Build a query to match mails send from "me" with "me" in BCC
(let ((bcc-query (+mu-long-query "bcc" "or" +my-addresses))
(from-query (+mu-long-query "from" "or" +my-addresses)))
(add-to-list
'mu4e-bookmarks
(list :name "My black copies" :query (format "maildir:/.*inbox/ and %s and %s" from-query bcc-query) :key ?k) t)))
;; `mu4e-alert' configuration
;; Use a nicer icon in alerts
(setq mu4e-alert-icon "/usr/share/icons/Papirus/64x64/apps/mail-client.svg")
(defun +mu4e-alert-helper-name-or-email (msg)
(let* ((from (car (plist-get msg :from)))
(name (plist-get from :name)))
(if (or (null name) (eq name ""))
(plist-get from :email)
name)))
(defun +mu4e-alert-grouped-mail-notif-formatter (mail-group _all-mails)
(when +mu4e-alert-bell-cmd
(start-process "mu4e-alert-bell" nil (car +mu4e-alert-bell-cmd) (cdr +mu4e-alert-bell-cmd)))
(let* ((filtered-mails (+filter
(lambda (msg)
(not (string-match-p "\\(junk\\|spam\\|trash\\|deleted\\)"
(downcase (plist-get msg :maildir)))))
mail-group))
(mail-count (length filtered-mails)))
(list
:title (format "You have %d unread email%s"
mail-count (if (> mail-count 1) "s" ""))
:body (concat
"• "
(+str-join
"\n• "
(mapcar
(lambda (msg)
(format "<b>%s</b>: %s"
(+mu4e-alert-helper-name-or-email msg)
(plist-get msg :subject)))
filtered-mails))))))
;; I use auto-hiding task manager, setting window
;; urgency shows the entier task bar (in KDE), which I find annoying.
(setq mu4e-alert-set-window-urgency nil
mu4e-alert-grouped-mail-notification-formatter #'+mu4e-alert-grouped-mail-notif-formatter)
;; Org-Msg stuff
;; org-msg-[signature|greeting-fmt] are separately set for each account
(setq mail-user-agent 'mu4e-user-agent) ;; Needed by OrgMsg
(require 'org-msg)
(setq org-msg-convert-citation t
org-msg-default-alternatives
'((new . (utf-8 html))
(reply-to-html . (utf-8 html))
(reply-to-text . (utf-8 html))))
(map! :map org-msg-edit-mode-map
:after org-msg
:n "G" #'org-msg-goto-body)
(map! :localleader
:map (mu4e-headers-mode-map mu4e-view-mode-map)
:desc "Open URL in Brave" "b" #'browse-url-chrome ;; Brave
:desc "Open URL in Firefox" "f" #'browse-url-firefox)
;; I like to always BCC myself
(defun +bbc-me ()
"Add my email to BCC."
(save-excursion (message-add-header (format "Bcc: %s\n" +my-bcc-trash))))
(add-hook 'mu4e-compose-mode-hook '+bbc-me)
;; Load my accounts
(load! "lisp/private/+mu4e-accounts.el")
;; iCalendar / Org
(mu4e-icalendar-setup)
(setq mu4e-icalendar-trash-after-reply nil
mu4e-icalendar-diary-file "~/Dropbox/Org/diary-invitations.org"
gnus-icalendar-org-capture-file "~/Dropbox/Org/notes.org"
gnus-icalendar-org-capture-headline '("Calendar"))
;; To enable optional iCalendar->Org sync functionality
;; NOTE: both the capture file and the headline(s) inside must already exist
(gnus-icalendar-org-setup))
The lisp/private/+mu4e-accounts.el
file includes Doom’s mu4e multi-account
configuration as follows:
(set-email-account!
"Work" ;; Account label
;; Mu4e folders
'((mu4e-sent-folder . "/work-dir/Sent")
(mu4e-drafts-folder . "/work-dir/Drafts")
(mu4e-trash-folder . "/work-dir/Trash")
(mu4e-refile-folder . "/work-dir/Archive")
;; Org-msg template (signature and greeting)
(org-msg-greeting-fmt . "Hello%s,")
(org-msg-signature . "
Regards,
#+begin_signature
-----
*Abdelhak BOUGOUFFA* \\\\
/PhD. Candidate in Robotics | R&D Engineer/ \\\\
/Paris-Saclay University - SATIE/MOSS | ez-Wheel/ \\\\
#+end_signature")
;; 'smtpmail' options, no need for these when using 'msmtp'
(smtpmail-smtp-user . "username@server.com")
(smtpmail-smtp-server . "smtps.server.com")
(smtpmail-stream-type . ssl)
(smtpmail-smtp-service . 465)
;; By default, `smtpmail' will try to send mails without authentication, and if rejected,
;; it tries to send credentials. This behavior broke my configuration. So I set this
;; variable to tell 'smtpmail' to require authentication for our server (using a regex).
(smtpmail-servers-requiring-authorization . "smtps\\.server\\.com"))
t) ;; Use as default/fallback account
;; Set another account
(set-email-account!
"Gmail"
'((mu4e-sent-folder . "/gmail-dir/Sent")
(mu4e-drafts-folder . "/gmail-dir/Drafts")
(mu4e-trash-folder . "/gmail-dir/Trash")
(mu4e-refile-folder . "/gmail-dir/Archive")
(org-msg-greeting-fmt . "Hello%s,")
(org-msg-signature . "-- SIGNATURE")
;; No need for these when using 'msmtp'
(smtpmail-smtp-user . "username@gmail.com")
(smtpmail-smtp-server . "smtp.googlemail.com")
(smtpmail-stream-type . starttls)
(smtpmail-smtp-service . 587)
...))
;; Tell Doom's mu4e module to override some commands to fix issues on Gmail accounts
(setq +mu4e-gmail-accounts '(("username@gmail.com" . "/gmail-dir")))
Dashboard
(after! mu4e
;; Fix icons
(defun +mu4e-initialise-icons ()
(setq mu4e-use-fancy-chars t
mu4e-headers-draft-mark (cons "D" (+mu4e-normalised-icon "edit" :set "material"))
mu4e-headers-flagged-mark (cons "F" (+mu4e-normalised-icon "flag" :set "material"))
mu4e-headers-new-mark (cons "N" (+mu4e-normalised-icon "file_download" :set "material" :color "dred"))
mu4e-headers-passed-mark (cons "P" (+mu4e-normalised-icon "forward" :set "material"))
mu4e-headers-replied-mark (cons "R" (+mu4e-normalised-icon "reply" :set "material"))
mu4e-headers-seen-mark (cons "S" "")
mu4e-headers-trashed-mark (cons "T" (+mu4e-normalised-icon "delete" :set "material"))
mu4e-headers-attach-mark (cons "a" (+mu4e-normalised-icon "attach_file" :set "material"))
mu4e-headers-encrypted-mark (cons "x" (+mu4e-normalised-icon "lock" :set "material"))
mu4e-headers-signed-mark (cons "s" (+mu4e-normalised-icon "verified_user" :set "material" :color "dpurple"))
mu4e-headers-unread-mark (cons "u" (+mu4e-normalised-icon "remove_red_eye" :set "material" :color "dred"))
mu4e-headers-list-mark (cons "l" (+mu4e-normalised-icon "list" :set "material"))
mu4e-headers-personal-mark (cons "p" (+mu4e-normalised-icon "person" :set "material"))
mu4e-headers-calendar-mark (cons "c" (+mu4e-normalised-icon "date_range" :set "material"))))
(+mu4e-initialise-icons))
Save all attachements
(after! mu4e
;; From https://github.com/sje30/emacs/blob/d7e21b94c79a5b6f244f33faff514036226e183c/mu4e-view-save-all-attachments.el
(defun +cleanse-subject (sub)
(replace-regexp-in-string "[^A-Z0-9]+" "-" (downcase sub)))
(defun +mu4e-view-save-all-attachments (&optional arg)
"Save all MIME parts from current mu4e gnus view buffer."
;; Copied from mu4e-view-save-attachments
(interactive "P")
(cl-assert (and (eq major-mode 'mu4e-view-mode)
(derived-mode-p 'gnus-article-mode)))
(let* ((msg (mu4e-message-at-point))
(id (+cleanse-subject (mu4e-message-field msg :subject)))
(attachdir (expand-file-name id mu4e-attachment-dir))
(parts (mu4e~view-gather-mime-parts))
(handles '())
(files '())
dir)
(mkdir attachdir t)
(dolist (part parts)
(let ((fname (or (cdr (assoc 'filename (assoc "attachment" (cdr part))))
(seq-find #'stringp
(mapcar (lambda (item) (cdr (assoc 'name item)))
(seq-filter 'listp (cdr part)))))))
(when fname
(push `(,fname . ,(cdr part)) handles)
(push fname files))))
(if files
(progn
(setq dir
(if arg (read-directory-name "Save to directory: ")
attachdir))
(cl-loop for (f . h) in handles
when (member f files)
do (mm-save-part-to-file h
(+file-name-incremental
(expand-file-name f dir)))))
(mu4e-message "No attached files found"))))
(map! :map mu4e-view-mode-map
:ne "P" #'+mu4e-view-save-all-attachments))
IRC
;; TODO: Not tangled
(defun +fetch-my-password (&rest params)
(require 'auth-source)
(let ((match (car (apply #'auth-source-search params))))
(if match
(let ((secret (plist-get match :secret)))
(if (functionp secret)
(funcall secret)
secret))
(error "Password not found for %S" params))))
(defun +my-nickserv-password (server)
(+fetch-my-password :user "abougouffa" :host "irc.libera.chat"))
(set-irc-server! "irc.libera.chat"
'(:tls t
:port 6697
:nick "abougouffa"
:sasl-password +my-nickserver-password
:channels ("#emacs")))
Multimedia
I like to use an MPD powered EMMS, so when I restart Emacs I do not lose my music.
MPD and MPC
;; Not sure if it is required!
(after! mpc
(setq mpc-host "localhost:6600"))
I like to launch the music daemon mpd
using Systemd, let’s define some commands
in Emacs to start/kill the server:
(defun +mpd-daemon-start ()
"Start MPD, connects to it and syncs the metadata cache."
(interactive)
(let ((mpd-daemon-running-p (+mpd-daemon-running-p)))
(unless mpd-daemon-running-p
;; Start the daemon if it is not already running.
(setq mpd-daemon-running-p (+systemd-start "mpd")))
(cond ((+mpd-daemon-running-p)
(+mpd-mpc-update)
(emms-player-mpd-connect)
(emms-cache-set-from-mpd-all)
(message "Connected to MPD!"))
(t
(warn "An error occured when trying to start Systemd mpd.service.")))))
(defun +mpd-daemon-stop ()
"Stops playback and kill the MPD daemon."
(interactive)
(emms-stop)
(+systemd-stop "mpd")
(message "MPD stopped!"))
(defun +mpd-daemon-running-p ()
"Check if the MPD service is running."
(+systemd-running-p "mpd"))
(defun +mpd-mpc-update ()
"Updates the MPD database synchronously."
(interactive)
(if (zerop (call-process "mpc" nil nil nil "update"))
(message "MPD database updated!")
(warn "An error occured when trying to update MPD database.")))
EMMS
Now, we configure EMMS to use MPD if it is present; otherwise, it uses whatever default backend EMMS is using.
(after! emms
;; EMMS basic configuration
(require 'emms-setup)
(when MPD-P
(require 'emms-player-mpd))
(emms-all)
(emms-default-players)
(setq emms-source-file-default-directory "~/Music/"
;; Load cover images
emms-browser-covers 'emms-browser-cache-thumbnail-async
emms-seek-seconds 5)
(if MPD-P
;; If using MPD as backend
(setq emms-player-list '(emms-player-mpd)
emms-info-functions '(emms-info-mpd)
emms-player-mpd-server-name "localhost"
emms-player-mpd-server-port "6600"
emms-player-mpd-music-directory (expand-file-name "~/Music"))
;; Use whatever backend EMMS is using by default (VLC in my machine)
(setq emms-info-functions '(emms-info-tinytag))) ;; use Tinytag, or '(emms-info-exiftool) for Exiftool
;; Keyboard shortcuts
(global-set-key (kbd "<XF86AudioPrev>") 'emms-previous)
(global-set-key (kbd "<XF86AudioNext>") 'emms-next)
(global-set-key (kbd "<XF86AudioPlay>") 'emms-pause)
(global-set-key (kbd "<XF86AudioPause>") 'emms-pause)
(global-set-key (kbd "<XF86AudioStop>") 'emms-stop)
;; Try to start MPD or connect to it if it is already started.
(when MPD-P
(emms-player-set emms-player-mpd 'regex
(emms-player-simple-regexp
"m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff"))
(add-hook 'emms-playlist-cleared-hook 'emms-player-mpd-clear)
(+mpd-daemon-start))
;; Activate EMMS in mode line
(emms-mode-line 1)
;; More descriptive track lines in playlists
;; From: https://www.emacswiki.org/emacs/EMMS#h5o-15
(defun +better-emms-track-description (track)
"Return a somewhat nice track description."
(let ((artist (emms-track-get track 'info-artist))
(album (emms-track-get track 'info-album))
(tracknumber (emms-track-get track 'info-tracknumber))
(title (emms-track-get track 'info-title)))
(cond
((or artist title)
(concat
(if (> (length artist) 0) artist "Unknown artist") ": "
(if (> (length album) 0) album "Unknown album") " - "
(if (> (length tracknumber) 0) (format "%02d. " (string-to-number tracknumber)) "")
(if (> (length title) 0) title "Unknown title")))
(t
(emms-track-simple-description track)))))
(setq emms-track-description-function '+better-emms-track-description)
;; Manage notifications, inspired by:
;; https://www.emacswiki.org/emacs/EMMS#h5o-9
;; https://www.emacswiki.org/emacs/EMMS#h5o-11
(cond
;; Choose D-Bus to disseminate messages, if available.
((and (require 'dbus nil t) (dbus-ping :session "org.freedesktop.Notifications"))
(setq +emms-notifier-function '+notify-via-freedesktop-notifications)
(require 'notifications))
;; Try to make use of KNotify if D-Bus isn't present.
((and window-system (executable-find "kdialog"))
(setq +emms-notifier-function '+notify-via-kdialog))
;; Use the message system otherwise
(t (setq +emms-notifier-function '+notify-via-messages)))
(setq +emms-notification-icon "/usr/share/icons/Papirus/64x64/apps/enjoy-music-player.svg")
(defun +notify-via-kdialog (title msg icon)
"Send notification with TITLE, MSG, and ICON via `KDialog'."
(call-process "kdialog"
nil nil nil
"--title" title
"--passivepopup" msg "5"
"--icon" icon))
(defun +notify-via-freedesktop-notifications (title msg icon)
"Send notification with TITLE, MSG, and ICON via `D-Bus'."
(notifications-notify
:title title
:body msg
:app-icon icon
:urgency 'low))
(defun +notify-via-messages (title msg icon)
"Send notification with TITLE, MSG to message. ICON is ignored."
(message "%s %s" title msg))
(add-hook 'emms-player-started-hook
(lambda () (funcall +emms-notifier-function
"EMMS is now playing:"
(emms-track-description (emms-playlist-current-selected-track))
+emms-notification-icon))))
EMPV
(package! empv
:recipe (:host github
:repo "isamert/empv.el")
:pin "49b25a3633bc362ee5fe84c8028b0412ade362c5")
(use-package! empv
:when MPV-P
:init
(map! :leader :prefix ("l m")
(:prefix ("v" . "empv")
:desc "Play" "p" #'empv-play
:desc "Seach Youtube" "y" #'consult-empv-youtube
:desc "Play radio" "r" #'empv-play-radio
:desc "Save current playlist" "s" #'+empv-save-playtlist-to-file))
:config
;; See https://docs.invidious.io/instances/
(setq empv-invidious-instance "https://invidious.projectsegfau.lt/api/v1"
empv-audio-dir "~/Music"
empv-video-dir "~/Videos"
empv-max-directory-search-depth 6
empv-radio-log-file (expand-file-name "logged-radio-songs.org" org-directory)
empv-audio-file-extensions '("webm" "mp3" "ogg" "wav" "m4a" "flac" "aac" "opus")
;; Links from https://www.radio-browser.info
empv-radio-channels
'(("El-Bahdja FM" . "http://webradio.tda.dz:8001/ElBahdja_64K.mp3")
("El-Chaabia" . "https://radio-dzair.net/proxy/chaabia?mp=/stream")
("Quran Radio" . "http://stream.radiojar.com/0tpy1h0kxtzuv")
("Algeria International" . "https://webradio.tda.dz/Internationale_64K.mp3")
("JOW Radio" . "https://str0.creacast.com/jowradio")
("Europe1" . "http://ais-live.cloud-services.paris:8000/europe1.mp3")
("France Iter" . "http://direct.franceinter.fr/live/franceinter-hifi.aac")
("France Info" . "http://direct.franceinfo.fr/live/franceinfo-midfi.mp3")
("France Culture" . "http://icecast.radiofrance.fr/franceculture-hifi.aac")
("France Musique" . "http://icecast.radiofrance.fr/francemusique-hifi.aac")
("FIP" . "http://icecast.radiofrance.fr/fip-hifi.aac")
("Beur FM" . "http://broadcast.infomaniak.ch/beurfm-high.aac")
("Skyrock" . "http://icecast.skyrock.net/s/natio_mp3_128k")))
(empv-playlist-loop-on)
;; Hacky palylist management (only supports saving playlist,
;; loading a playlist can be achieved using `empv-play-file')
(defun +empv--dl-playlist (playlist &optional dist)
(let ((default-directory
(or dist
(let ((d (expand-file-name "empv-downloads" empv-audio-dir)))
(unless (file-directory-p d) (mkdir d t)) d)))
(vids (+filter
'identity ;; Filter nils
(mapcar
(lambda (item)
(when-let
((vid (when (string-match
(rx (seq "watch?v=" (group-n 1 (one-or-more (or alnum "_" "-")))))
item)
(match-string 1 item))))
vid))
playlist)))
(proc-name "empv-yt-dlp"))
(unless (zerop (length vids))
(message "Downloading %d songs to %s" (length vids) default-directory)
(when (get-process proc-name)
(kill-process proc-name))
(make-process :name proc-name
:buffer (format "*%s*" proc-name)
:command (append
(list
(executable-find "yt-dlp")
"--no-abort-on-error"
"--no-colors"
"--extract-audio"
"--no-progress"
"-f" "bestaudio")
vids)
:sentinel (lambda (prc event)
(when (string= event "finished\n")
(message "Finished downloading playlist files!")))))))
(defun +empv-download-playtlist-files (&optional path)
(interactive "DSave download playlist files to: ")
(empv--playlist-apply #'+empv--dl-playlist path)))
Keybindings
Lastly, let’s define the keybindings for these commands, under <leader> l m
.
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :app emms)
:prefix ("m" . "media")
:desc "Playlist go" "g" #'emms-playlist-mode-go
:desc "Add playlist" "D" #'emms-add-playlist
:desc "Toggle random playlist" "r" #'emms-toggle-random-playlist
:desc "Add directory" "d" #'emms-add-directory
:desc "Add file" "f" #'emms-add-file
:desc "Smart browse" "b" #'emms-smart-browse
:desc "Play/Pause" "p" #'emms-pause
:desc "Start" "S" #'emms-start
:desc "Stop" "s" #'emms-stop))
Then we add MPD related keybindings if MPD is used.
(map! :leader :prefix ("l m")
(:when (and (modulep! :app emms) MPD-P)
:prefix ("m" . "mpd/mpc")
:desc "Start daemon" "s" #'+mpd-daemon-start
:desc "Stop daemon" "k" #'+mpd-daemon-stop
:desc "EMMS player (MPD update)" "R" #'emms-player-mpd-update-all-reset-cache
:desc "Update database" "u" #'+mpd-mpc-update))
Cycle song information in mode line
I found a useful package named emms-mode-line-cycle
which permits to do this;
however, it has not been updated since a while, it uses some obsolete functions
to draw icon in mode line, so I forked it, got rid of the problematic parts, and
added some minor stuff.
(package! emms-mode-line-cycle
:recipe (:host github
:repo "abougouffa/emms-mode-line-cycle")
:pin "7a269c9aef9ece7ecf997f6abb9cd3818403b0bb")
(use-package! emms-mode-line-cycle
:after emms
:config
(setq emms-mode-line-cycle-max-width 15
emms-mode-line-cycle-additional-space-num 4
emms-mode-line-cycle-any-width-p nil
emms-mode-line-cycle-velocity 4)
;; Some music files do not have metadata, by default, the track title
;; will be the full file path, so, if I detect what seems to be an absolute
;; path, I trim the directory part and get only the file name.
(setq emms-mode-line-cycle-current-title-function
(lambda ()
(let ((name (emms-track-description (emms-playlist-current-selected-track))))
(if (file-name-absolute-p name) (file-name-base name) name))))
;; Mode line formatting settings
;; This format complements the 'emms-mode-line-format' one.
(setq emms-mode-line-format " ⟨⏵ %s⟩" ;; 𝅘𝅥𝅮 ⏵ ⏸
;; To hide the playing time without stopping the cycling.
emms-playing-time-display-format "")
(defun +emms-mode-line-toggle-format-hook ()
"Toggle the 'emms-mode-line-fotmat' string, when playing or paused."
(setq emms-mode-line-format (concat " ⟨" (if emms-player-paused-p "⏸" "⏵") " %s⟩"))
;; Force a sync to get the right song name over MPD in mode line
(when MPD-P (emms-player-mpd-sync-from-mpd))
;; Trigger a forced update of mode line (useful when pausing)
(emms-mode-line-alter-mode-line))
;; Hook the function to the 'emms-player-paused-hook'
(add-hook 'emms-player-paused-hook '+emms-mode-line-toggle-format-hook)
(emms-mode-line-cycle 1))
Maxima
The Maxima CAS cames bundled with three Emacs modes: maxima
, imaxima
and
emaxima
; installed by default in "/usr/share/emacs/site-lisp/maxima"
.
Maxima
The emacsmirror/maxima seems more up-to-date, and supports completion via
Company, so let’s install it from GitHub. Note that, normally, we don’t need to
specify a recipe; however, installing it directly seems to not install
company-maxima.el
and poly-maxima.el
.
(package! maxima
:recipe (:host github
:repo "emacsmirror/maxima"
:files (:defaults
"keywords"
"company-maxima.el"
"poly-maxima.el"))
:pin "1334f44725bd80a265de858d652f3fde4ae401fa")
(use-package! maxima
:when MAXIMA-P
:commands (maxima-mode maxima-inferior-mode maxima)
:init
(require 'straight) ;; to use `straight-build-dir' and `straight-base-dir'
(setq maxima-font-lock-keywords-directory ;; a workaround to undo the straight workaround!
(expand-file-name (format "straight/%s/maxima/keywords" straight-build-dir) straight-base-dir))
;; The `maxima-hook-function' setup `company-maxima'.
(add-hook 'maxima-mode-hook #'maxima-hook-function)
(add-hook 'maxima-inferior-mode-hook #'maxima-hook-function)
(add-to-list 'auto-mode-alist '("\\.ma[cx]\\'" . maxima-mode)))
IMaxima
For the imaxima
(Maxima with image support), the emacsattic/imaxima seems
outdated compared to the imaxima
package of the official Maxima distribution, so
let’s install imaxima
from the source code of Maxima, hosted on Sourceforge
git.code.sf.net/p/maxima/code. The package files are stored in the repository’s
subdirectory interfaces/emacs/imaxima
.
;; Use the `imaxima' package bundled with the official Maxima distribution.
(package! imaxima
:recipe (:host nil ;; Unsupported host, we will specify the complete repo link
:repo "https://git.code.sf.net/p/maxima/code"
:files ("interfaces/emacs/imaxima/*"))
:pin "f1e60b2ec1ae447845b113e8f3aa77fb4b7e4289")
(use-package! imaxima
:when MAXIMA-P
:commands (imaxima imath-mode)
:init
(setq imaxima-use-maxima-mode-flag nil ;; otherwise, it don't render equations with LaTeX.
imaxima-scale-factor 2.0)
;; Hook the `maxima-inferior-mode' to get Company completion.
(add-hook 'imaxima-startup-hook #'maxima-inferior-mode))
FriCAS
The FriCAS cames bundled with an Emacs mode, let’s load it.
(use-package! fricas
:when FRICAS-P
:load-path FRICAS-DIR
:commands (fricas-mode fricas-eval fricas))
Roam
Org-roam is nice by itself, but there are so extra nice packages which integrate with it.
(use-package! websocket
:after org-roam-ui)
(use-package! org-roam-ui
:commands org-roam-ui-open
:config (setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t
org-roam-ui-open-on-start t))
Basic settings
(use-package! org-roam
:init
(setq org-roam-directory "~/Dropbox/Org/slip-box"
org-roam-db-location (expand-file-name "org-roam.db" org-roam-directory)))
Let’s disable org-roam
if the directory doesn’t exist.
(package! org-roam :disable t)
Mode line file name
All those numbers! It’s messy. Let’s adjust this similarly that I have in the window title
(defadvice! doom-modeline--buffer-file-name-roam-aware-a (orig-fun)
:around #'doom-modeline-buffer-file-name ; takes no args
(if (s-contains-p org-roam-directory (or buffer-file-name ""))
(replace-regexp-in-string
"\\(?:^\\|.*/\\)\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)[0-9]*-"
"🢔(\\1-\\2-\\3) "
(subst-char-in-string ?_ ? buffer-file-name))
(funcall orig-fun)))
Org Roam Capture template
(after! org-roam
(setq org-roam-capture-ref-templates
'(("r" "ref" plain "%?"
:if-new (file+head "web/%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+created: %U\n\n${body}\n")
:unnarrowed t))))
View notes in Deft
;; From https://org-roam.discourse.group/t/configure-deft-title-stripping-to-hide-org-roam-template-headers/478/10
(use-package! deft
:after org
:bind
("C-c n d" . deft)
:init
(setq deft-directory org-roam-directory
;; deft-recursive t
deft-use-filter-string-for-filename t
deft-default-extension "org")
:config
(defun +deft-parse-title (file contents)
"Parse the given FILE and CONTENTS and determine the title.
If `deft-use-filename-as-title' is nil, the title is taken to
be the first non-empty line of the FILE. Else the base name of the FILE is
used as title."
(let ((begin (string-match "^#\\+[tT][iI][tT][lL][eE]: .*$" contents)))
(if begin
(string-trim (substring contents begin (match-end 0)) "#\\+[tT][iI][tT][lL][eE]: *" "[\n\t ]+")
(deft-base-filename file))))
(advice-add 'deft-parse-title :override #'+deft-parse-title)
(setq deft-strip-summary-regexp
(concat "\\("
"[\n\t]" ;; blank
"\\|^#\\+[[:alpha:]_]+:.*$" ;; org-mode metadata
"\\|^:PROPERTIES:\n\\(.+\n\\)+:END:\n" ;; org-roam ID
"\\|\\[\\[\\(.*\\]\\)" ;; any link
"\\)")))
Programming
CSV rainbow
Stolen from here.
(after! csv-mode
;; TODO: Need to fix the case of two commas, example "a,b,,c,d"
(require 'cl-lib)
(require 'color)
(map! :localleader
:map csv-mode-map
"R" #'+csv-rainbow)
(defun +csv-rainbow (&optional separator)
(interactive (list (when current-prefix-arg (read-char "Separator: "))))
(font-lock-mode 1)
(let* ((separator (or separator ?\,))
(n (count-matches (string separator) (point-at-bol) (point-at-eol)))
(colors (cl-loop for i from 0 to 1.0 by (/ 2.0 n)
collect (apply #'color-rgb-to-hex
(color-hsl-to-rgb i 0.3 0.5)))))
(cl-loop for i from 2 to n by 2
for c in colors
for r = (format "^\\([^%c\n]+%c\\)\\{%d\\}" separator separator i)
do (font-lock-add-keywords nil `((,r (1 '(face (:foreground ,c))))))))))
;; provide CSV mode setup
;; (add-hook 'csv-mode-hook (lambda () (+csv-rainbow)))
Vimrc
(package! vimrc-mode
:recipe (:host github
:repo "mcandre/vimrc-mode")
:pin "13bc150a870d5d4a95f1111e4740e2b22813c30e")
(use-package! vimrc-mode
:mode "\\.vim\\(rc\\)?\\'")
ESS
View data frames better with
(package! ess-view :pin "925cafd876e2cc37bc756bb7fcf3f34534b457e2")
Python IDE
(package! elpy :pin "ae7919d94659eb26d4146d4c3422c5f4c3610837")
(use-package! elpy
:hook ((elpy-mode . flycheck-mode)
(elpy-mode . (lambda ()
(set (make-local-variable 'company-backends)
'((elpy-company-backend :with company-yasnippet))))))
:config
(elpy-enable))
Semgrep
Lightweight static analysis for many languages. Find bug variants with patterns that look like source code.
(package! semgrep
:disable t
:recipe (:host github
:repo "Ruin0x11/semgrep.el")
:pin "3313f38ed7d23947992e19f1e464c6d544124144")
GNU Octave
Files with the .m
extension gets recognized automatically as Objective-C files.
I’ve never used Objective-C before, so let’s change it to be recognized as
Octave/Matlab files.
(add-to-list 'auto-mode-alist '("\\.m\\'" . octave-mode))
(defun +octave-eval-last-sexp ()
"Evaluate Octave sexp before point and print value into current buffer."
(interactive)
(inferior-octave t)
(let ((print-escape-newlines nil)
(opoint (point)))
(prin1
(save-excursion
(forward-sexp -1)
(inferior-octave-send-list-and-digest
(list (concat (buffer-substring-no-properties (point) opoint)
"\n")))
(mapconcat 'identity inferior-octave-output-list "\n")))))
(defun +eros-octave-eval-last-sexp ()
"Wrapper for `+octave-eval-last-sexp' that overlays results."
(interactive)
(eros--eval-overlay
(+octave-eval-last-sexp)
(point)))
(map! :localleader
:map (octave-mode-map)
(:prefix ("e" . "eval")
:desc "Eval and print last sexp" "e" #'+eros-octave-eval-last-sexp))
ROS
File extensions
Add ROS specific file formats:
(add-to-list 'auto-mode-alist '("\\.rviz\\'" . conf-unix-mode))
(add-to-list 'auto-mode-alist '("\\.urdf\\'" . xml-mode))
(add-to-list 'auto-mode-alist '("\\.xacro\\'" . xml-mode))
(add-to-list 'auto-mode-alist '("\\.launch\\'" . xml-mode))
;; Use gdb-script-mode for msg and srv files
(add-to-list 'auto-mode-alist '("\\.msg\\'" . gdb-script-mode))
(add-to-list 'auto-mode-alist '("\\.srv\\'" . gdb-script-mode))
(add-to-list 'auto-mode-alist '("\\.action\\'" . gdb-script-mode))
ROS bags
Mode to view ROS .bag
files. Taken from code-iai/ros_emacs_utils.
(when ROSBAG-P
(define-derived-mode rosbag-view-mode
fundamental-mode "Rosbag view mode"
"Major mode for viewing ROS bag files."
(let ((f (buffer-file-name)))
(let ((buffer-read-only nil))
(erase-buffer)
(message "Calling rosbag info")
(call-process "rosbag" nil (current-buffer) nil
"info" f)
(set-buffer-modified-p nil))
(view-mode)
(set-visited-file-name nil t)))
;; rosbag view mode
(add-to-list 'auto-mode-alist '("\\.bag$" . rosbag-view-mode)))
ros.el
I found this awesome ros.el
package made by Max Beutelspacher, which
facilitate working with ROS machines, supports ROS1 and ROS2, with local
workspaces or remote ones (over Trump!).
;; `ros.el' depends on `with-shell-interpreter' among other packages
;; See: https://github.com/DerBeutlin/ros.el/blob/master/Cask
(package! with-shell-interpreter :pin "3fd1ea892e44f7fe6f86df2b5c0a0a1e0f3913fa")
(package! ros
:recipe (:host github
:repo "DerBeutlin/ros.el")
:pin "f66d2177b00b277a36c058549c477d854148623c")
Now, we configure the ROS1/ROS2 workspaces to work on. But before that, we need
to install some tools on the ROS machine, and build the workspace for the first
time using colcon build
, the repository contains example Docker files for Noetic
and Foxy.
(use-package! ros
:init
(map! :leader
:prefix ("l" . "custom")
:desc "Hydra ROS" "r" #'hydra-ros-main/body)
:commands (hydra-ros-main/body ros-set-workspace)
:config
(setq ros-workspaces
(list (ros-dump-workspace
:tramp-prefix (format "/docker:%s@%s:" "ros" "ros-machine")
:workspace "~/ros_ws"
:extends '("/opt/ros/noetic/"))
(ros-dump-workspace
:tramp-prefix (format "/ssh:%s@%s:" "swd_sk" "172.16.96.42")
:workspace "~/ros_ws"
:extends '("/opt/ros/noetic/"))
(ros-dump-workspace
:tramp-prefix (format "/ssh:%s@%s:" "swd_sk" "172.16.96.42")
:workspace "~/ros2_ws"
:extends '("/opt/ros/foxy/")))))
Scheme
(after! geiser
(setq geiser-default-implementation 'guile
geiser-chez-binary "chez-scheme")) ;; default is "scheme"
Embedded systems
Embed.el
Some embedded systems development tools.
TODO: Try to integrate embedded debuggers adapters with dap-mode
:
(package! embed
:recipe (:host github
:repo "sjsch/embed-el")
:pin "8df65777450c6c70a418d1bd2ba87ad590377b47")
(use-package! embed
:commands (embed-openocd-start
embed-openocd-stop
embed-openocd-gdb
embed-openocd-flash)
:init
(map! :leader :prefix ("l" . "custom")
(:when (modulep! :tools debugger +lsp)
:prefix ("e" . "embedded")
:desc "Start OpenOCD" "o" #'embed-openocd-start
:desc "Stop OpenOCD" "O" #'embed-openocd-stop
:desc "OpenOCD GDB" "g" #'embed-openocd-gdb
:desc "OpenOCD flash" "f" #'embed-openocd-flash)))
Arduino
(package! arduino-mode
:recipe (:host github
:repo "bookest/arduino-mode")
:pin "3e2bad4569ad26e929e6db2cbcff0d6d36812698")
Bitbake (Yocto)
Add support for Yocto Project files.
(package! bitbake-modes
:recipe (:host nil
:repo "https://bitbucket.org/olanilsson/bitbake-modes")
:pin "a042118fd2010ef203a11e1de14e7537f8184a78")
(use-package! bitbake-modes
:commands (wks-mode
mmm-mode
bb-sh-mode
bb-scc-mode
bitbake-mode
conf-bitbake-mode
bitbake-task-log-mode))
Git & VC
Magit
(after! code-review
(setq code-review-auth-login-marker 'forge))
Granular diff-highlights for all hunks
(after! magit
;; Disable if it causes performance issues
(setq magit-diff-refine-hunk t))
Gravatars
(after! magit
;; Show gravatars
(setq magit-revision-show-gravatars '("^Author: " . "^Commit: ")))
WIP Company for commit messages
(package! company-conventional-commits
:recipe `(:local-repo ,(expand-file-name "lisp/company-conventional-commits" doom-user-dir)))
(use-package! company-conventional-commits
:after (magit company)
:config
(add-hook
'git-commit-setup-hook
(lambda ()
(add-to-list 'company-backends 'company-conventional-commits))))
Pretty graph
(package! magit-pretty-graph
:recipe (:host github
:repo "georgek/magit-pretty-graph")
:pin "26dc5535a20efe781b172bac73f14a5ebe13efa9")
(use-package! magit-pretty-graph
:after magit
:init
(setq magit-pg-command
(concat "git --no-pager log"
" --topo-order --decorate=full"
" --pretty=format:\"%H%x00%P%x00%an%x00%ar%x00%s%x00%d\""
" -n 2000")) ;; Increase the default 100 limit
(map! :localleader
:map (magit-mode-map)
:desc "Magit pretty graph" "p" (cmd! (magit-pg-repo (magit-toplevel)))))
Repo
This adds Emacs integration of repo
, The Multiple Git Repository Tool. Make sure
the repo tool is installed, if not, pacman -S repo
on Arch-based distributions,
or directly with:
REPO_PATH="$HOME/.local/bin/repo"
curl "https://storage.googleapis.com/git-repo-downloads/repo" > "${REPO_PATH}"
chmod a+x "${REPO_PATH}"
(package! repo :pin "e504aa831bfa38ddadce293face28b3c9d9ff9b7")
(use-package! repo
:when REPO-P
:commands repo-status)
Blamer
Display Git information (author, date, message…) for current line
(package! blamer
:recipe (:host github
:repo "artawower/blamer.el")
:pin "99b43779341af0d924bfe2a9103993a6b9e3d3b2")
(use-package! blamer
:commands (blamer-mode)
;; :hook ((prog-mode . blamer-mode))
:custom
(blamer-idle-time 0.3)
(blamer-min-offset 60)
(blamer-prettify-time-p t)
(blamer-entire-formatter " %s")
(blamer-author-formatter " %s ")
(blamer-datetime-formatter "[%s], ")
(blamer-commit-formatter "“%s”")
:custom-face
(blamer-face ((t :foreground "#7a88cf"
:background nil
:height 125
:italic t))))
Assembly
Add some packages for better assembly coding.
(package! nasm-mode :pin "65ca6546fc395711fac5b3b4299e76c2303d43a8")
(package! haxor-mode :pin "6fa25a8e6b6a59481bc0354c2fe1e0ed53cbdc91")
(package! mips-mode :pin "98795cdc81979821ac35d9f94ce354cd99780c67")
(package! riscv-mode :pin "8e335b9c93de93ed8dd063d702b0f5ad48eef6d7")
(package! x86-lookup :pin "1573d61cc4457737b94624598a891c837fb52c16")
(use-package! nasm-mode
:mode "\\.[n]*\\(asm\\|s\\)\\'")
;; Get Haxor VM from https://github.com/krzysztof-magosa/haxor
(use-package! haxor-mode
:mode "\\.hax\\'")
(use-package! mips-mode
:mode "\\.mips\\'")
(use-package! riscv-mode
:mode "\\.riscv\\'")
(use-package! x86-lookup
:commands (x86-lookup)
:config
(when (modulep! :tools pdf)
(setq x86-lookup-browse-pdf-function 'x86-lookup-browse-pdf-pdf-tools))
;; Get manual from https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
(setq x86-lookup-pdf (expand-file-name "x86-lookup/325383-sdm-vol-2abcd.pdf" doom-data-dir)))
Disaster
(package! disaster :pin "0c13bd244cc43773af81e52ce73a55f199d58a61")
(use-package! disaster
:commands (disaster)
:init
(setq disaster-assembly-mode 'nasm-mode)
(map! :localleader
:map (c++-mode-map c-mode-map fortran-mode)
:desc "Disaster" "d" #'disaster))
Devdocs
(package! devdocs
:recipe (:host github
:repo "astoff/devdocs.el"
:files ("*.el"))
:pin "61ce83b79dc64e2f99d7f016a09b97e14b331459")
(use-package! devdocs
:commands (devdocs-lookup devdocs-install)
:config
(setq devdocs-data-dir (expand-file-name "devdocs" doom-data-dir)))
Systemd
For editing systemd unit files.
(package! systemd :pin "b6ae63a236605b1c5e1069f7d3afe06ae32a7bae")
(package! journalctl-mode :pin "c5bca1a5f42d2fe2a00fdf52fe480137ace971d3")
(use-package! journalctl-mode
:commands (journalctl
journalctl-boot
journalctl-unit
journalctl-user-unit)
:init
(map! :map journalctl-mode-map
:nv "J" #'journalctl-next-chunk
:nv "K" #'journalctl-previous-chunk))
PKGBUILD
(package! pkgbuild-mode :pin "9525be8ecbd3a0d0bc7cc27e6d0f403e111aa067")
(use-package! pkgbuild-mode
:commands (pkgbuild-mode)
:mode "/PKGBUILD$")
Franca IDL
Add support for Franca Interface Definition Language.
(package! franca-idl
:recipe (:host github
:repo "zeph1e/franca-idl.el")
:pin "12703ee42533bd851a1d911609020f71eb31204a")
(use-package! franca-idl
:commands franca-idl-mode)
LaTeX
(package! aas
:recipe (:host github
:repo "ymarco/auto-activating-snippets")
:pin "566944e3b336c29d3ac11cd739a954c9d112f3fb")
(use-package! aas
:commands aas-mode)
Flycheck + Projectile
WIP: Not working atm!
(package! flycheck-projectile
:recipe (:host github
:repo "nbfalcon/flycheck-projectile")
:pin "ce6e9e8793a55dace13d5fa13badab2dca3b5ddb")
(use-package! flycheck-projectile
:commands flycheck-projectile-list-errors)
Graphviz
Graphviz is a nice method of visualizing simple graphs, based on th DOT graph
description language (*.dot
/ *.gv
files).
(package! graphviz-dot-mode :pin "6e96a89762760935a7dff6b18393396f6498f976")
(use-package! graphviz-dot-mode
:commands graphviz-dot-mode
:mode ("\\.dot\\'" "\\.gv\\'")
:init
(after! org
(setcdr (assoc "dot" org-src-lang-modes) 'graphviz-dot))
:config
(require 'company-graphviz-dot))
Modula-II
Gaius Mulley is doing a great job, bringing Modula-II support to GCC, he also
created a new mode for Modula-II with extended features. The mode is included
with the GNU Modula 2 source code, and can be downloaded separately from the Git
repository, from here gm2-mode.el. I added (provide 'gm2-mode)
to the
gm2-mode.el
.
(package! gm2-mode
:recipe `(:local-repo ,(expand-file-name "lisp/gm2-mode" doom-user-dir)))
Mermaid
(package! mermaid-mode :pin "a98a9e733b1da1e6a19e68c1db4367bf46283479")
(package! ob-mermaid
:recipe (:host github
:repo "arnm/ob-mermaid")
:pin "b4ce25699e3ebff054f523375d1cf5a17bd0dbaf")
(use-package! mermaid-mode
:commands mermaid-mode
:mode "\\.mmd\\'")
(use-package! ob-mermaid
:after org
:init
(after! org
(add-to-list 'org-babel-load-languages '(mermaid . t))))
The V Programming Language
(package! v-mode :pin "a701f4cedfff91cf4bcd17c9a2cd16a49f942743")
(use-package! v-mode
:mode ("\\(\\.v?v\\|\\.vsh\\)$" . 'v-mode)
:config
(map! :localleader
:map (v-mode-map)
:desc "v-format-buffer" "f" #'v-format-buffer
:desc "v-menu" "m" #'v-menu))
Inspector
(package! inspector
:recipe (:host github
:repo "mmontone/emacs-inspector")
:pin "f30b735fca1c3979e693c4c76cac85885c07d8ab")
(use-package! inspector
:commands (inspect-expression inspect-last-sexp))
Office
Org additional packages
To avoid problems in the (after! org)
section.
(unpin! org-roam) ;; To avoid problems with org-roam-ui
(package! websocket :pin "82b370602fa0158670b1c6c769f223159affce9b")
(package! org-roam-ui :pin "16a8da9e5107833032893bc4c0680b368ac423ac")
(package! org-wild-notifier :pin "9392b06d20b2f88e45a41bea17bb2f10f24fd19c")
(package! org-fragtog :pin "c675563af3f9ab5558cfd5ea460e2a07477b0cfd")
(package! org-appear :pin "60ba267c5da336e75e603f8c7ab3f44e6f4e4dac")
(package! org-super-agenda :pin "f4f528985397c833c870967884b013cf91a1da4a")
(package! doct :pin "506c22f365b75f5423810c4933856802554df464")
(package! citar-org-roam
:recipe (:host github
:repo "emacs-citar/citar-org-roam")
:pin "29688b89ac3bf78405fa0dce7e17965aa8fe0dff")
(package! org-menu
:recipe (:host github
:repo "sheijk/org-menu")
:pin "9cd10161c2b50dfef581f3d0441683eeeae6be59")
(package! caldav
:recipe (:host github
:repo "dengste/org-caldav")
:pin "8569941a0a5a9393ba51afc8923fd7b77b73fa7a")
(package! org-ol-tree
:recipe (:host github
:repo "Townk/org-ol-tree")
:pin "207c748aa5fea8626be619e8c55bdb1c16118c25")
(package! org-modern
:recipe (:host github
:repo "minad/org-modern")
:pin "828cf100c62fc9dfb50152c192ac3a968c1b54bc")
(package! org-bib
:recipe (:host github
:repo "rougier/org-bib-mode")
:pin "fed9910186e5e579c2391fb356f55ae24093b55a")
(package! academic-phrases
:recipe (:host github
:repo "nashamri/academic-phrases")
:pin "25d9cf67feac6359cb213f061735e2679c84187f")
(package! phscroll
:recipe (:host github
:repo "misohena/phscroll")
:pin "65e00c89f078997e1a5665d069ad8b1e3b851d49")
Org mode
Intro
Because this section is fairly expensive to initialize, we’ll wrap it in a
(after! ...)
block.
(after! org
<<org-conf>>
)
Behavior
Tweaking defaults
Org basics
(setq org-directory "~/Dropbox/Org/" ; let's put files here
org-use-property-inheritance t ; it's convenient to have properties inherited
org-log-done 'time ; having the time an item is done sounds convenient
org-list-allow-alphabetical t ; have a. A. a) A) list bullets
org-export-in-background nil ; run export processes in external emacs process
org-export-async-debug t
org-tags-column 0
org-catch-invisible-edits 'smart ;; try not to accidently do weird stuff in invisible regions
org-export-with-sub-superscripts '{} ;; don't treat lone _ / ^ as sub/superscripts, require _{} / ^{}
org-pretty-entities-include-sub-superscripts nil
org-auto-align-tags nil
org-special-ctrl-a/e t
org-startup-indented t ;; Enable 'org-indent-mode' by default, override with '+#startup: noindent' for big files
org-insert-heading-respect-content t)
Babel
I also like the :comments
header-argument, so let’s make that a
default.
(setq org-babel-default-header-args
'((:session . "none")
(:results . "replace")
(:exports . "code")
(:cache . "no")
(:noweb . "no")
(:hlines . "no")
(:tangle . "no")
(:comments . "link")))
Babel is really annoying when it comes to working with Scheme (via Geiser), it
keeps asking about which Scheme implementation to use, I tried to set this as a
local variable (using )
and .dir-locals.el
, but it didn’t work. This hack should solve the problem now!
;; stolen from https://github.com/yohan-pereira/.emacs#babel-config
(defun +org-confirm-babel-evaluate (lang body)
(not (string= lang "scheme"))) ;; Don't ask for scheme
(setq org-confirm-babel-evaluate #'+org-confirm-babel-evaluate)
EVIL
There also seem to be a few keybindings which use hjkl
, but miss arrow key
equivalents.
(map! :map evil-org-mode-map
:after evil-org
:n "g <up>" #'org-backward-heading-same-level
:n "g <down>" #'org-forward-heading-same-level
:n "g <left>" #'org-up-element
:n "g <right>" #'org-down-element)
TODOs
(setq org-todo-keywords
'((sequence "IDEA(i)" "TODO(t)" "NEXT(n)" "PROJ(p)" "STRT(s)" "WAIT(w)" "HOLD(h)" "|" "DONE(d)" "KILL(k)")
(sequence "[ ](T)" "[-](S)" "|" "[X](D)")
(sequence "|" "OKAY(o)" "YES(y)" "NO(n)")))
(setq org-todo-keyword-faces
'(("IDEA" . (:foreground "goldenrod" :weight bold))
("NEXT" . (:foreground "IndianRed1" :weight bold))
("STRT" . (:foreground "OrangeRed" :weight bold))
("WAIT" . (:foreground "coral" :weight bold))
("KILL" . (:foreground "DarkGreen" :weight bold))
("PROJ" . (:foreground "LimeGreen" :weight bold))
("HOLD" . (:foreground "orange" :weight bold))))
(defun +log-todo-next-creation-date (&rest ignore)
"Log NEXT creation time in the property drawer under the key 'ACTIVATED'"
(when (and (string= (org-get-todo-state) "NEXT")
(not (org-entry-get nil "ACTIVATED")))
(org-entry-put nil "ACTIVATED" (format-time-string "[%Y-%m-%d]"))))
(add-hook 'org-after-todo-state-change-hook #'+log-todo-next-creation-date)
Tags
(setq org-tag-persistent-alist
'((:startgroup . nil)
("home" . ?h)
("research" . ?r)
("work" . ?w)
(:endgroup . nil)
(:startgroup . nil)
("tool" . ?o)
("dev" . ?d)
("report" . ?p)
(:endgroup . nil)
(:startgroup . nil)
("easy" . ?e)
("medium" . ?m)
("hard" . ?a)
(:endgroup . nil)
("urgent" . ?u)
("key" . ?k)
("bonus" . ?b)
("ignore" . ?i)
("noexport" . ?x)))
(setq org-tag-faces
'(("home" . (:foreground "goldenrod" :weight bold))
("research" . (:foreground "goldenrod" :weight bold))
("work" . (:foreground "goldenrod" :weight bold))
("tool" . (:foreground "IndianRed1" :weight bold))
("dev" . (:foreground "IndianRed1" :weight bold))
("report" . (:foreground "IndianRed1" :weight bold))
("urgent" . (:foreground "red" :weight bold))
("key" . (:foreground "red" :weight bold))
("easy" . (:foreground "green4" :weight bold))
("medium" . (:foreground "orange" :weight bold))
("hard" . (:foreground "red" :weight bold))
("bonus" . (:foreground "goldenrod" :weight bold))
("ignore" . (:foreground "Gray" :weight bold))
("noexport" . (:foreground "LimeGreen" :weight bold))))
Agenda
Set files for org-agenda
(setq org-agenda-files
(list (expand-file-name "inbox.org" org-directory)
(expand-file-name "agenda.org" org-directory)
(expand-file-name "gcal-agenda.org" org-directory)
(expand-file-name "notes.org" org-directory)
(expand-file-name "projects.org" org-directory)
(expand-file-name "archive.org" org-directory)))
Apply some styling on the standard agenda:
;; Agenda styling
(setq org-agenda-block-separator ?─
org-agenda-time-grid
'((daily today require-timed)
(800 1000 1200 1400 1600 1800 2000)
" ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄")
org-agenda-current-time-string
"⭠ now ─────────────────────────────────────────────────")
Super agenda
Configure org-super-agenda
(use-package! org-super-agenda
:defer t
:config
(org-super-agenda-mode)
:init
(setq org-agenda-skip-scheduled-if-done t
org-agenda-skip-deadline-if-done t
org-agenda-include-deadlines t
org-agenda-block-separator nil
org-agenda-tags-column 100 ;; from testing this seems to be a good value
org-agenda-compact-blocks t)
(setq org-agenda-custom-commands
'(("o" "Overview"
((agenda "" ((org-agenda-span 'day)
(org-super-agenda-groups
'((:name "Today"
:time-grid t
:date today
:todo "TODAY"
:scheduled today
:order 1)))))
(alltodo "" ((org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Next to do" :todo "NEXT" :order 1)
(:name "Important" :tag "Important" :priority "A" :order 6)
(:name "Due Today" :deadline today :order 2)
(:name "Due Soon" :deadline future :order 8)
(:name "Overdue" :deadline past :face error :order 7)
(:name "Assignments" :tag "Assignment" :order 10)
(:name "Issues" :tag "Issue" :order 12)
(:name "Emacs" :tag "Emacs" :order 13)
(:name "Projects" :tag "Project" :order 14)
(:name "Research" :tag "Research" :order 15)
(:name "To read" :tag "Read" :order 30)
(:name "Waiting" :todo "WAIT" :order 20)
(:name "University" :tag "Univ" :order 32)
(:name "Trivial" :priority<= "E" :tag ("Trivial" "Unimportant") :todo ("SOMEDAY") :order 90)
(:discard (:tag ("Chore" "Routine" "Daily"))))))))))))
Calendar
Google calendar (org-gcal
)
I store my org-gcal
configuration privately, it contains something like this:
(setq org-gcal-client-id "<SOME_ID>.apps.googleusercontent.com"
org-gcal-client-secret "<SOME_SECRET>"
org-gcal-fetch-file-alist '(("<USERNAME>@gmail.com" . "~/Dropbox/Org/gcal-agenda.org")))
(after! org-gcal
(load! "lisp/private/+org-gcal.el"))
TODO CalDAV
Need to be configured, see the GitHub repo.
(use-package! caldav
:commands (org-caldav-sync))
Capture
Set capture files
(setq +org-capture-emails-file (expand-file-name "inbox.org" org-directory)
+org-capture-todo-file (expand-file-name "inbox.org" org-directory)
+org-capture-projects-file (expand-file-name "projects.org" org-directory))
Let’s set up some org-capture
templates, and make them visually nice to access.
(use-package! doct
:commands (doct))
(after! org-capture
<<prettify-capture>>
(defun +doct-icon-declaration-to-icon (declaration)
"Convert :icon declaration to icon"
(let ((name (pop declaration))
(set (intern (concat "all-the-icons-" (plist-get declaration :set))))
(face (intern (concat "all-the-icons-" (plist-get declaration :color))))
(v-adjust (or (plist-get declaration :v-adjust) 0.01)))
(apply set `(,name :face ,face :v-adjust ,v-adjust))))
(defun +doct-iconify-capture-templates (groups)
"Add declaration's :icon to each template group in GROUPS."
(let ((templates (doct-flatten-lists-in groups)))
(setq doct-templates
(mapcar (lambda (template)
(when-let* ((props (nthcdr (if (= (length template) 4) 2 5) template))
(spec (plist-get (plist-get props :doct) :icon)))
(setf (nth 1 template) (concat (+doct-icon-declaration-to-icon spec)
"\t"
(nth 1 template))))
template)
templates))))
(setq doct-after-conversion-functions '(+doct-iconify-capture-templates))
(defun set-org-capture-templates ()
(setq org-capture-templates
(doct `(("Personal todo" :keys "t"
:icon ("checklist" :set "octicon" :color "green")
:file +org-capture-todo-file
:prepend t
:headline "Inbox"
:type entry
:template ("* TODO %?"
"%i %a"))
("Personal note" :keys "n"
:icon ("sticky-note-o" :set "faicon" :color "green")
:file +org-capture-todo-file
:prepend t
:headline "Inbox"
:type entry
:template ("* %?"
"%i %a"))
("Email" :keys "e"
:icon ("envelope" :set "faicon" :color "blue")
:file +org-capture-todo-file
:prepend t
:headline "Inbox"
:type entry
:template ("* TODO %^{type|reply to|contact} %\\3 %? ✉️"
"Send an email %^{urgancy|soon|ASAP|anon|at some point|eventually} to %^{recipiant}"
"about %^{topic}"
"%U %i %a"))
("Interesting" :keys "i"
:icon ("eye" :set "faicon" :color "lcyan")
:file +org-capture-todo-file
:prepend t
:headline "Interesting"
:type entry
:template ("* [ ] %{desc}%? :%{i-type}:"
"%i %a")
:children (("Webpage" :keys "w"
:icon ("globe" :set "faicon" :color "green")
:desc "%(org-cliplink-capture) "
:i-type "read:web")
("Article" :keys "a"
:icon ("file-text" :set "octicon" :color "yellow")
:desc ""
:i-type "read:reaserch")
("Information" :keys "i"
:icon ("info-circle" :set "faicon" :color "blue")
:desc ""
:i-type "read:info")
("Idea" :keys "I"
:icon ("bubble_chart" :set "material" :color "silver")
:desc ""
:i-type "idea")))
("Tasks" :keys "k"
:icon ("inbox" :set "octicon" :color "yellow")
:file +org-capture-todo-file
:prepend t
:headline "Tasks"
:type entry
:template ("* TODO %? %^G%{extra}"
"%i %a")
:children (("General Task" :keys "k"
:icon ("inbox" :set "octicon" :color "yellow")
:extra "")
("Task with deadline" :keys "d"
:icon ("timer" :set "material" :color "orange" :v-adjust -0.1)
:extra "\nDEADLINE: %^{Deadline:}t")
("Scheduled Task" :keys "s"
:icon ("calendar" :set "octicon" :color "orange")
:extra "\nSCHEDULED: %^{Start time:}t")))
("Project" :keys "p"
:icon ("repo" :set "octicon" :color "silver")
:prepend t
:type entry
:headline "Inbox"
:template ("* %{time-or-todo} %?"
"%i"
"%a")
:file ""
:custom (:time-or-todo "")
:children (("Project-local todo" :keys "t"
:icon ("checklist" :set "octicon" :color "green")
:time-or-todo "TODO"
:file +org-capture-project-todo-file)
("Project-local note" :keys "n"
:icon ("sticky-note" :set "faicon" :color "yellow")
:time-or-todo "%U"
:file +org-capture-project-notes-file)
("Project-local changelog" :keys "c"
:icon ("list" :set "faicon" :color "blue")
:time-or-todo "%U"
:heading "Unreleased"
:file +org-capture-project-changelog-file)))
("\tCentralised project templates"
:keys "o"
:type entry
:prepend t
:template ("* %{time-or-todo} %?"
"%i"
"%a")
:children (("Project todo"
:keys "t"
:prepend nil
:time-or-todo "TODO"
:heading "Tasks"
:file +org-capture-central-project-todo-file)
("Project note"
:keys "n"
:time-or-todo "%U"
:heading "Notes"
:file +org-capture-central-project-notes-file)
("Project changelog"
:keys "c"
:time-or-todo "%U"
:heading "Unreleased"
:file +org-capture-central-project-changelog-file)))))))
(set-org-capture-templates)
(unless (display-graphic-p)
(add-hook 'server-after-make-frame-hook
(defun org-capture-reinitialise-hook ()
(when (display-graphic-p)
(set-org-capture-templates)
(remove-hook 'server-after-make-frame-hook
#'org-capture-reinitialise-hook))))))
It would also be nice to improve how the capture dialogue looks
(defun org-capture-select-template-prettier (&optional keys)
"Select a capture template, in a prettier way than default
Lisp programs can force the template by setting KEYS to a string."
(let ((org-capture-templates
(or (org-contextualize-keys
(org-capture-upgrade-templates org-capture-templates)
org-capture-templates-contexts)
'(("t" "Task" entry (file+headline "" "Tasks")
"* TODO %?\n %u\n %a")))))
(if keys
(or (assoc keys org-capture-templates)
(error "No capture template referred to by \"%s\" keys" keys))
(org-mks org-capture-templates
"Select a capture template\n━━━━━━━━━━━━━━━━━━━━━━━━━"
"Template key: "
`(("q" ,(concat (all-the-icons-octicon "stop" :face 'all-the-icons-red :v-adjust 0.01) "\tAbort")))))))
(advice-add 'org-capture-select-template :override #'org-capture-select-template-prettier)
(defun org-mks-pretty (table title &optional prompt specials)
"Select a member of an alist with multiple keys. Prettified.
TABLE is the alist which should contain entries where the car is a string.
There should be two types of entries.
1. prefix descriptions like (\"a\" \"Description\")
This indicates that `a' is a prefix key for multi-letter selection, and
that there are entries following with keys like \"ab\", \"ax\"…
2. Select-able members must have more than two elements, with the first
being the string of keys that lead to selecting it, and the second a
short description string of the item.
The command will then make a temporary buffer listing all entries
that can be selected with a single key, and all the single key
prefixes. When you press the key for a single-letter entry, it is selected.
When you press a prefix key, the commands (and maybe further prefixes)
under this key will be shown and offered for selection.
TITLE will be placed over the selection in the temporary buffer,
PROMPT will be used when prompting for a key. SPECIALS is an
alist with (\"key\" \"description\") entries. When one of these
is selected, only the bare key is returned."
(save-window-excursion
(let ((inhibit-quit t)
(buffer (org-switch-to-buffer-other-window "*Org Select*"))
(prompt (or prompt "Select: "))
case-fold-search
current)
(unwind-protect
(catch 'exit
(while t
(setq-local evil-normal-state-cursor (list nil))
(erase-buffer)
(insert title "\n\n")
(let ((des-keys nil)
(allowed-keys '("\C-g"))
(tab-alternatives '("\s" "\t" "\r"))
(cursor-type nil))
;; Populate allowed keys and descriptions keys
;; available with CURRENT selector.
(let ((re (format "\\`%s\\(.\\)\\'"
(if current (regexp-quote current) "")))
(prefix (if current (concat current " ") "")))
(dolist (entry table)
(pcase entry
;; Description.
(`(,(and key (pred (string-match re))) ,desc)
(let ((k (match-string 1 key)))
(push k des-keys)
;; Keys ending in tab, space or RET are equivalent.
(if (member k tab-alternatives)
(push "\t" allowed-keys)
(push k allowed-keys))
(insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) (propertize "›" 'face 'font-lock-comment-face) " " desc "…" "\n")))
;; Usable entry.
(`(,(and key (pred (string-match re))) ,desc . ,_)
(let ((k (match-string 1 key)))
(insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) " " desc "\n")
(push k allowed-keys)))
(_ nil))))
;; Insert special entries, if any.
(when specials
(insert "─────────────────────────\n")
(pcase-dolist (`(,key ,description) specials)
(insert (format "%s %s\n" (propertize key 'face '(bold all-the-icons-red)) description))
(push key allowed-keys)))
;; Display UI and let user select an entry or
;; a sublevel prefix.
(goto-char (point-min))
(unless (pos-visible-in-window-p (point-max))
(org-fit-window-to-buffer))
(let ((pressed (org--mks-read-key allowed-keys
prompt
(not (pos-visible-in-window-p (1- (point-max)))))))
(setq current (concat current pressed))
(cond
((equal pressed "\C-g") (user-error "Abort"))
;; Selection is a prefix: open a new menu.
((member pressed des-keys))
;; Selection matches an association: return it.
((let ((entry (assoc current table)))
(and entry (throw 'exit entry))))
;; Selection matches a special entry: return the
;; selection prefix.
((assoc current specials) (throw 'exit current))
(t (error "No entry available")))))))
(when buffer (kill-buffer buffer))))))
(advice-add 'org-mks :override #'org-mks-pretty)
The org-capture bin is rather nice, but I’d be nicer with a smaller frame, and no modeline.
(setf (alist-get 'height +org-capture-frame-parameters) 15)
;; (alist-get 'name +org-capture-frame-parameters) "❖ Capture") ;; ATM hardcoded in other places, so changing breaks stuff
(setq +org-capture-fn
(lambda ()
(interactive)
(set-window-parameter nil 'mode-line-format 'none)
(org-capture)))
Snippet Helpers
I often want to set src-block
headers, and it’s a pain to:
- type them out
- remember what the accepted values are
- oh, and specifying the same language again and again
We can solve this in three steps:
- having one-letter snippets, conditioned on
(point)
being within a src header - creating a nice prompt showing accepted values and the current default
- pre-filling the
src-block
language with the last language used
For header args, the keys I’ll use are:
r
for:results
e
for:exports
v
for:eval
s
for:session
d
for:dir
(defun +yas/org-src-header-p ()
"Determine whether `point' is within a src-block header or header-args."
(pcase (org-element-type (org-element-context))
('src-block (< (point) ; before code part of the src-block
(save-excursion (goto-char (org-element-property :begin (org-element-context)))
(forward-line 1)
(point))))
('inline-src-block (< (point) ; before code part of the inline-src-block
(save-excursion (goto-char (org-element-property :begin (org-element-context)))
(search-forward "]{")
(point))))
('keyword (string-match-p "^header-args" (org-element-property :value (org-element-context))))))
Now let’s write a function we can reference in YASnippets to produce a nice interactive way to specify header arguments.
(defun +yas/org-prompt-header-arg (arg question values)
"Prompt the user to set ARG header property to one of VALUES with QUESTION.
The default value is identified and indicated. If either default is selected,
or no selection is made: nil is returned."
(let* ((src-block-p (not (looking-back "^#\\+property:[ \t]+header-args:.*" (line-beginning-position))))
(default
(or
(cdr (assoc arg
(if src-block-p
(nth 2 (org-babel-get-src-block-info t))
(org-babel-merge-params
org-babel-default-header-args
(let ((lang-headers
(intern (concat "org-babel-default-header-args:"
(+yas/org-src-lang)))))
(when (boundp lang-headers) (eval lang-headers t)))))))
""))
default-value)
(setq values (mapcar
(lambda (value)
(if (string-match-p (regexp-quote value) default)
(setq default-value
(concat value " "
(propertize "(default)" 'face 'font-lock-doc-face)))
value))
values))
(let ((selection (consult--read question values :default default-value)))
(unless (or (string-match-p "(default)$" selection)
(string= "" selection))
selection))))
Finally, we fetch the language information for new source blocks.
Since we’re getting this info, we might as well go a step further and also
provide the ability to determine the most popular language in the buffer that
doesn’t have any header-args
set for it (with #+properties
).
(defun +yas/org-src-lang ()
"Try to find the current language of the src/header at `point'.
Return nil otherwise."
(let ((context (org-element-context)))
(pcase (org-element-type context)
('src-block (org-element-property :language context))
('inline-src-block (org-element-property :language context))
('keyword (when (string-match "^header-args:\\([^ ]+\\)" (org-element-property :value context))
(match-string 1 (org-element-property :value context)))))))
(defun +yas/org-last-src-lang ()
"Return the language of the last src-block, if it exists."
(save-excursion
(beginning-of-line)
(when (re-search-backward "^[ \t]*#\\+begin_src" nil t)
(org-element-property :language (org-element-context)))))
(defun +yas/org-most-common-no-property-lang ()
"Find the lang with the most source blocks that has no global header-args, else nil."
(let (src-langs header-langs)
(save-excursion
(goto-char (point-min))
(while (re-search-forward "^[ \t]*#\\+begin_src" nil t)
(push (+yas/org-src-lang) src-langs))
(goto-char (point-min))
(while (re-search-forward "^[ \t]*#\\+property: +header-args" nil t)
(push (+yas/org-src-lang) header-langs)))
(setq src-langs
(mapcar #'car
;; sort alist by frequency (desc.)
(sort
;; generate alist with form (value . frequency)
(cl-loop for (n . m) in (seq-group-by #'identity src-langs)
collect (cons n (length m)))
(lambda (a b) (> (cdr a) (cdr b))))))
(car (cl-set-difference src-langs header-langs :test #'string=))))
Translate capital keywords to lower case
Everyone used to use #+CAPITAL
keywords. Then people realised that #+lowercase
is actually both marginally easier and visually nicer, so now the capital
version is just used in the manual.
Org is standardized on lower case. Uppercase is used in the manual as a poor man’s bold, and supported for historical reasons. — Nicolas Goaziou
(defun +org-syntax-convert-keyword-case-to-lower ()
"Convert all #+KEYWORDS to #+keywords."
(interactive)
(save-excursion
(goto-char (point-min))
(let ((count 0)
(case-fold-search nil))
(while (re-search-forward "^[ \t]*#\\+[A-Z_]+" nil t)
(unless (s-matches-p "RESULTS" (match-string 0))
(replace-match (downcase (match-string 0)) t)
(setq count (1+ count))))
(message "Replaced %d occurances" count))))
Org notifier
Add support for org-wild-notifier
.
(use-package! org-wild-notifier
:hook (org-load . org-wild-notifier-mode)
:config
(setq org-wild-notifier-alert-time '(60 30)))
Org menu
(use-package! org-menu
:commands (org-menu)
:init
(map! :localleader
:map org-mode-map
:desc "Org menu" "M" #'org-menu))
LSP in src
blocks
(when (and (modulep! :tools lsp) (not (modulep! :tools lsp +eglot)))
(cl-defmacro +lsp-org-babel-enable (lang)
"Support LANG in org source code block."
;; (setq centaur-lsp 'lsp-mode)
(cl-check-type lang stringp)
(let* ((edit-pre (intern (format "org-babel-edit-prep:%s" lang)))
(intern-pre (intern (format "lsp--%s" (symbol-name edit-pre)))))
`(progn
(defun ,intern-pre (info)
(let ((file-name (->> info caddr (alist-get :file))))
(unless file-name
(setq file-name (make-temp-file "babel-lsp-")))
(setq buffer-file-name file-name)
(lsp-deferred)))
(put ',intern-pre 'function-documentation
(format "Enable lsp-mode in the buffer of org source block (%s)."
(upcase ,lang)))
(if (fboundp ',edit-pre)
(advice-add ',edit-pre :after ',intern-pre)
(progn
(defun ,edit-pre (info)
(,intern-pre info))
(put ',edit-pre 'function-documentation
(format "Prepare local buffer environment for org source block (%s)."
(upcase ,lang))))))))
(defvar +org-babel-lang-list
'("go" "python" "ipython" "bash" "sh"))
(dolist (lang +org-babel-lang-list)
(eval `(+lsp-org-babel-enable ,lang))))
Custom links
Sub-figures
This defines a new link type subfig
to enable exporting sub-figures to LaTeX,
taken form “Export subfigures to LaTeX (and HTML)”.
(org-link-set-parameters
"subfig"
:follow (lambda (file) (find-file file))
:face '(:foreground "chocolate" :weight bold :underline t)
:display 'full
:export
(lambda (file desc backend)
(when (eq backend 'latex)
(if (string-match ">(\\(.+\\))" desc)
(concat "\\begin{subfigure}[b]"
"\\caption{" (replace-regexp-in-string "\s+>(.+)" "" desc) "}"
"\\includegraphics" "[" (match-string 1 desc) "]" "{" file "}" "\\end{subfigure}")
(format "\\begin{subfigure}\\includegraphics{%s}\\end{subfigure}" desc file)))))
Example of usage:
#+caption: Lorem impsum dolor
#+attr_latex: :options \centering
#+begin_figure
[[subfig:img1.jpg][Caption of img1 >(width=.3\textwidth)]]
[[subfig:img2.jpg][Caption of img2 >(width=.3\textwidth)]]
[[subfig:img3.jpg][Caption of img3 >(width=.6\textwidth)]]
#+end_figure
LaTeX inline markup
Needs to make a ?, with this hack you can write
[[latex:textsc][Some text]]
.
(org-add-link-type
"latex" nil
(lambda (path desc format)
(cond
((eq format 'html)
(format "<span class=\"%s\">%s</span>" path desc))
((eq format 'latex)
(format "\\%s{%s}" path desc)))))
Visuals
Here I try to do two things: improve the styling of the various documents, via font changes etc., and also propagate colours from the current theme.
Font display
Headings
Let’s make the title and the headings a bit bigger:
(custom-set-faces!
'(org-document-title :height 1.2))
(custom-set-faces!
'(outline-1 :weight extra-bold :height 1.25)
'(outline-2 :weight bold :height 1.15)
'(outline-3 :weight bold :height 1.12)
'(outline-4 :weight semi-bold :height 1.09)
'(outline-5 :weight semi-bold :height 1.06)
'(outline-6 :weight semi-bold :height 1.03)
'(outline-8 :weight semi-bold)
'(outline-9 :weight semi-bold))
Deadlines
It seems reasonable to have deadlines in the error face when they’re passed.
(setq org-agenda-deadline-faces
'((1.001 . error)
(1.000 . org-warning)
(0.500 . org-upcoming-deadline)
(0.000 . org-upcoming-distant-deadline)))
Font styling
We can then have quote blocks stand out a bit more by making them italic.
(setq org-fontify-quote-and-verse-blocks t)
While org-hide-emphasis-markers
is very nice, it can sometimes make edits which
occur at the border a bit more fiddley. We can improve this situation without
sacrificing visual amenities with the org-appear
package.
(use-package! org-appear
:hook (org-mode . org-appear-mode)
:config
(setq org-appear-autoemphasis t
org-appear-autosubmarkers t
org-appear-autolinks nil)
;; for proper first-time setup, `org-appear--set-elements'
;; needs to be run after other hooks have acted.
(run-at-time nil nil #'org-appear--set-elements))
Inline blocks
(setq org-inline-src-prettify-results '("⟨" . "⟩")
doom-themes-org-fontify-special-tags nil)
Org Modern
(use-package! org-modern
:hook (org-mode . org-modern-mode)
:config
(setq org-modern-star '("◉" "○" "◈" "◇" "✳" "◆" "✸" "▶")
org-modern-table-vertical 2
org-modern-table-horizontal 4
org-modern-list '((43 . "➤") (45 . "–") (42 . "•"))
org-modern-footnote (cons nil (cadr org-script-display))
org-modern-priority t
org-modern-block t
org-modern-block-fringe nil
org-modern-horizontal-rule t
org-modern-keyword
'((t . t)
("title" . "𝙏")
("subtitle" . "𝙩")
("author" . "𝘼")
("email" . "@")
("date" . "𝘿")
("lastmod" . "✎")
("property" . "☸")
("options" . "⌥")
("startup" . "⏻")
("macro" . "𝓜")
("bind" . #("" 0 1 (display (raise -0.1))))
("bibliography" . "")
("print_bibliography" . #("" 0 1 (display (raise -0.1))))
("cite_export" . "⮭")
("print_glossary" . #("ᴬᶻ" 0 1 (display (raise -0.1))))
("glossary_sources" . #("" 0 1 (display (raise -0.14))))
("export_file_name" . "⇒")
("include" . "⇤")
("setupfile" . "⇐")
("html_head" . "🅷")
("html" . "🅗")
("latex_class" . "🄻")
("latex_class_options" . #("🄻" 1 2 (display (raise -0.14))))
("latex_header" . "🅻")
("latex_header_extra" . "🅻⁺")
("latex" . "🅛")
("beamer_theme" . "🄱")
("beamer_color_theme" . #("🄱" 1 2 (display (raise -0.12))))
("beamer_font_theme" . "🄱𝐀")
("beamer_header" . "🅱")
("beamer" . "🅑")
("attr_latex" . "🄛")
("attr_html" . "🄗")
("attr_org" . "⒪")
("name" . "⁍")
("header" . "›")
("caption" . "☰")
("RESULTS" . "🠶")
("language" . "𝙇")
("hugo_base_dir" . "𝐇")
("latex_compiler" . "⟾")
("results" . "🠶")
("filetags" . "#")
("created" . "⏱")
("export_select_tags" . "✔")
("export_exclude_tags" . "❌")))
;; Change faces
(custom-set-faces! '(org-modern-tag :inherit (region org-modern-label)))
(custom-set-faces! '(org-modern-statistics :inherit org-checkbox-statistics-todo)))
Not let’s remove the overlap between the substitutions we set here and those
that Doom applies via :ui ligatures
and :lang org
.
(when (modulep! :ui ligatures)
(defadvice! +org-init-appearance-h--no-ligatures-a ()
:after #'+org-init-appearance-h
(set-ligatures! 'org-mode
:name nil
:src_block nil
:src_block_end nil
:quote nil
:quote_end nil)))
We’ll bind this to O
on the org-mode
localleader, and manually apply a PR
recognising the pgtk window system.
(use-package! org-ol-tree
:commands org-ol-tree
:config
(setq org-ol-tree-ui-icon-set
(if (and (display-graphic-p)
(fboundp 'all-the-icons-material))
'all-the-icons
'unicode))
(org-ol-tree-ui--update-icon-set))
(map! :localleader
:map org-mode-map
:desc "Outline" "O" #'org-ol-tree)
Image previews
(defvar +org-responsive-image-percentage 0.4)
(defvar +org-responsive-image-width-limits '(400 . 700)) ;; '(min-width . max-width)
(defun +org--responsive-image-h ()
(when (eq major-mode 'org-mode)
(setq org-image-actual-width
(max (car +org-responsive-image-width-limits)
(min (cdr +org-responsive-image-width-limits)
(truncate (* (window-pixel-width) +org-responsive-image-percentage)))))))
(add-hook 'window-configuration-change-hook #'+org--responsive-image-h)
List bullet sequence
I think it makes sense to have list bullets change with depth
(setq org-list-demote-modify-bullet
'(("+" . "-")
("-" . "+")
("*" . "+")
("1." . "a.")))
Symbols
;; Org styling, hide markup etc.
(setq org-hide-emphasis-markers t
org-pretty-entities t
org-ellipsis " ↩"
org-hide-leading-stars t)
;; org-priority-highest ?A
;; org-priority-lowest ?E
;; org-priority-faces
;; '((?A . 'all-the-icons-red)
;; (?B . 'all-the-icons-orange)
;; (?C . 'all-the-icons-yellow)
;; (?D . 'all-the-icons-green)
;; (?E . 'all-the-icons-blue)))
LaTeX fragments
Prettier highlighting
First off, we want those fragments to look good.
(setq org-highlight-latex-and-related '(native script entities))
(require 'org-src)
(add-to-list 'org-src-block-faces '("latex" (:inherit default :extend t)))
Prettier rendering
Since we can, instead of making the background color match the default
face,
let’s make it transparent.
(setq org-format-latex-options
(plist-put org-format-latex-options :background "Transparent"))
;; Can be dvipng, dvisvgm, imagemagick
(setq org-preview-latex-default-process 'dvisvgm)
;; Define a function to set the format latex scale (to be reused in hooks)
(defun +org-format-latex-set-scale (scale)
(setq-local org-format-latex-options
(plist-put org-format-latex-options :scale scale)))
;; Set the default scale
(+org-format-latex-set-scale 1.4)
Better equation numbering
Numbered equations all have (1) as the number for fragments with vanilla
org-mode
. This code (from scimax) injects the correct numbers into the previews,
so they look good.
This hack is not properly working right now!, it seems to work only with
align
blocks. NEEDS INVESTIGATION.
(defun +parse-the-fun (str)
"Parse the LaTeX environment STR.
Return an AST with newlines counts in each level."
(let (ast)
(with-temp-buffer
(insert str)
(goto-char (point-min))
(while (re-search-forward
(rx "\\"
(group (or "\\" "begin" "end" "nonumber"))
(zero-or-one "{" (group (zero-or-more not-newline)) "}"))
nil t)
(let ((cmd (match-string 1))
(env (match-string 2)))
(cond ((string= cmd "begin")
(push (list :env (intern env)) ast))
((string= cmd "\\")
(let ((curr (pop ast)))
(push (plist-put curr :newline (1+ (or (plist-get curr :newline) 0))) ast)))
((string= cmd "nonumber")
(let ((curr (pop ast)))
(push (plist-put curr :nonumber (1+ (or (plist-get curr :nonumber) 0))) ast)))
((string= cmd "end")
(let ((child (pop ast))
(parent (pop ast)))
(push (plist-put parent :childs (cons child (plist-get parent :childs))) ast)))))))
(plist-get (car ast) :childs)))
(defun +scimax-org-renumber-environment (orig-func &rest args)
"A function to inject numbers in LaTeX fragment previews."
(let ((results '())
(counter -1))
(setq results
(cl-loop for (begin . env) in
(org-element-map (org-element-parse-buffer) 'latex-environment
(lambda (env)
(cons
(org-element-property :begin env)
(org-element-property :value env))))
collect
(cond
((and (string-match "\\\\begin{equation}" env)
(not (string-match "\\\\tag{" env)))
(cl-incf counter)
(cons begin counter))
((string-match "\\\\begin{align}" env)
(cl-incf counter)
(let ((p (car (+parse-the-fun env))))
;; Parse the `env', count new lines in the align env as equations, unless
(cl-incf counter (- (or (plist-get p :newline) 0)
(or (plist-get p :nonumber) 0))))
(cons begin counter))
(t
(cons begin nil)))))
(when-let ((number (cdr (assoc (point) results))))
(setf (car args)
(concat
(format "\\setcounter{equation}{%s}\n" number)
(car args)))))
(apply orig-func args))
(defun +scimax-toggle-latex-equation-numbering (&optional enable)
"Toggle whether LaTeX fragments are numbered."
(interactive)
(if (or enable (not (get '+scimax-org-renumber-environment 'enabled)))
(progn
(advice-add 'org-create-formula-image :around #'+scimax-org-renumber-environment)
(put '+scimax-org-renumber-environment 'enabled t)
(message "LaTeX numbering enabled."))
(advice-remove 'org-create-formula-image #'+scimax-org-renumber-environment)
(put '+scimax-org-renumber-environment 'enabled nil)
(message "LaTeX numbering disabled.")))
(defun +scimax-org-inject-latex-fragment (orig-func &rest args)
"Advice function to inject latex code before and/or after the equation in a latex fragment.
You can use this to set \\mathversion{bold} for example to make
it bolder. The way it works is by defining
:latex-fragment-pre-body and/or :latex-fragment-post-body in the
variable `org-format-latex-options'. These strings will then be
injected before and after the code for the fragment before it is
made into an image."
(setf (car args)
(concat
(or (plist-get org-format-latex-options :latex-fragment-pre-body) "")
(car args)
(or (plist-get org-format-latex-options :latex-fragment-post-body) "")))
(apply orig-func args))
(defun +scimax-toggle-inject-latex ()
"Toggle whether you can insert latex in fragments."
(interactive)
(if (not (get '+scimax-org-inject-latex-fragment 'enabled))
(progn
(advice-add 'org-create-formula-image :around #'+scimax-org-inject-latex-fragment)
(put '+scimax-org-inject-latex-fragment 'enabled t)
(message "Inject latex enabled"))
(advice-remove 'org-create-formula-image #'+scimax-org-inject-latex-fragment)
(put '+scimax-org-inject-latex-fragment 'enabled nil)
(message "Inject latex disabled")))
;; Enable renumbering by default
(+scimax-toggle-latex-equation-numbering t)
Fragtog
Hook org-fragtog-mode
to org-mode
.
(use-package! org-fragtog
:hook (org-mode . org-fragtog-mode))
Org plot
We can use some variables in org-plot
to use the current doom theme
colors.
(after! org-plot
(defun org-plot/generate-theme (_type)
"Use the current Doom theme colours to generate a GnuPlot preamble."
(format "
fgt = \"textcolor rgb '%s'\" # foreground text
fgat = \"textcolor rgb '%s'\" # foreground alt text
fgl = \"linecolor rgb '%s'\" # foreground line
fgal = \"linecolor rgb '%s'\" # foreground alt line
# foreground colors
set border lc rgb '%s'
# change text colors of tics
set xtics @fgt
set ytics @fgt
# change text colors of labels
set title @fgt
set xlabel @fgt
set ylabel @fgt
# change a text color of key
set key @fgt
# line styles
set linetype 1 lw 2 lc rgb '%s' # red
set linetype 2 lw 2 lc rgb '%s' # blue
set linetype 3 lw 2 lc rgb '%s' # green
set linetype 4 lw 2 lc rgb '%s' # magenta
set linetype 5 lw 2 lc rgb '%s' # orange
set linetype 6 lw 2 lc rgb '%s' # yellow
set linetype 7 lw 2 lc rgb '%s' # teal
set linetype 8 lw 2 lc rgb '%s' # violet
# palette
set palette maxcolors 8
set palette defined ( 0 '%s',\
1 '%s',\
2 '%s',\
3 '%s',\
4 '%s',\
5 '%s',\
6 '%s',\
7 '%s' )
"
(doom-color 'fg)
(doom-color 'fg-alt)
(doom-color 'fg)
(doom-color 'fg-alt)
(doom-color 'fg)
;; colours
(doom-color 'red)
(doom-color 'blue)
(doom-color 'green)
(doom-color 'magenta)
(doom-color 'orange)
(doom-color 'yellow)
(doom-color 'teal)
(doom-color 'violet)
;; duplicated
(doom-color 'red)
(doom-color 'blue)
(doom-color 'green)
(doom-color 'magenta)
(doom-color 'orange)
(doom-color 'yellow)
(doom-color 'teal)
(doom-color 'violet)))
(defun org-plot/gnuplot-term-properties (_type)
(format "background rgb '%s' size 1050,650"
(doom-color 'bg)))
(setq org-plot/gnuplot-script-preamble #'org-plot/generate-theme
org-plot/gnuplot-term-extra #'org-plot/gnuplot-term-properties))
Large tables
Use Partial Horizontal Scroll to display long tables without breaking them.
(use-package! org-phscroll
:hook (org-mode . org-phscroll-mode))
Bibliography
BibTeX
(setq bibtex-completion-bibliography +my/biblio-libraries-list
bibtex-completion-library-path +my/biblio-storage-list
bibtex-completion-notes-path +my/biblio-notes-path
bibtex-completion-notes-template-multiple-files "* ${author-or-editor}, ${title}, ${journal}, (${year}) :${=type=}: \n\nSee [[cite:&${=key=}]]\n"
bibtex-completion-additional-search-fields '(keywords)
bibtex-completion-display-formats
'((article . "${=has-pdf=:1}${=has-note=:1} ${year:4} ${author:36} ${title:*} ${journal:40}")
(inbook . "${=has-pdf=:1}${=has-note=:1} ${year:4} ${author:36} ${title:*} Chapter ${chapter:32}")
(incollection . "${=has-pdf=:1}${=has-note=:1} ${year:4} ${author:36} ${title:*} ${booktitle:40}")
(inproceedings . "${=has-pdf=:1}${=has-note=:1} ${year:4} ${author:36} ${title:*} ${booktitle:40}")
(t . "${=has-pdf=:1}${=has-note=:1} ${year:4} ${author:36} ${title:*}"))
bibtex-completion-pdf-open-function
(lambda (fpath)
(call-process "open" nil 0 nil fpath)))
Org-bib
A mode to work with annotated bibliography in Org-Mode. See the repo for an example.
(use-package! org-bib
:commands (org-bib-mode))
Org-cite
(after! oc
(setq org-cite-csl-styles-dir +my/biblio-styles-path)
;; org-cite-global-bibliography +my/biblio-libraries-list)
(defun +org-ref-to-org-cite ()
"Simple conversion of org-ref citations to org-cite syntax."
(interactive)
(save-excursion
(goto-char (point-min))
(while (re-search-forward "\\[cite\\(.*\\):\\([^]]*\\)\\]" nil t)
(let* ((old (substring (match-string 0) 1 (1- (length (match-string 0)))))
(new (s-replace "&" "@" old)))
(message "Replaced citation %s with %s" old new)
(replace-match new))))))
Citar
(after! citar
(setq citar-library-paths +my/biblio-storage-list
citar-notes-paths (list +my/biblio-notes-path)
citar-bibliography +my/biblio-libraries-list
citar-symbol-separator " ")
(when (display-graphic-p)
(setq citar-symbols
`((file ,(all-the-icons-octicon "file-pdf" :face 'error) . " ")
(note ,(all-the-icons-octicon "file-text" :face 'warning) . " ")
(link ,(all-the-icons-octicon "link-external" :face 'org-link) . " ")))))
(use-package! citar-org-roam
:after citar org-roam
:no-require
:config (citar-org-roam-mode)
:init
;; Modified form: https://jethrokuan.github.io/org-roam-guide/
(defun +org-roam-node-from-cite (entry-key)
(interactive (list (citar-select-ref)))
(let ((title (citar-format--entry
"${author editor} (${date urldate}) :: ${title}"
(citar-get-entry entry-key))))
(org-roam-capture- :templates
'(("r" "reference" plain
"%?"
:if-new (file+head "references/${citekey}.org"
":properties:
:roam_refs: [cite:@${citekey}]
🔚
#+title: ${title}\n")
:immediate-finish t
:unnarrowed t))
:info (list :citekey entry-key)
:node (org-roam-node-create :title title)
:props '(:finalize find-file)))))
Exporting
General settings
By default, Org only exports the first three levels of headings as headings, the rest is considered as paragraphs. Let’s increase this to 5 levels.
(setq org-export-headline-levels 5)
Let’s make use of the :ignore:
tag from ox-extra
, which provides a way to ignore
exporting a heading, while exporting the content residing under it (different
from :noexport:
).
(require 'ox-extra)
(ox-extras-activate '(ignore-headlines))
(setq org-export-creator-string
(format "Made with Emacs %s and Org %s" emacs-version (org-release)))
LaTeX export
Compiling
;; `org-latex-compilers' contains a list of possible values for the `%latex' argument.
(setq org-latex-pdf-process
'("latexmk -shell-escape -pdf -quiet -f -%latex -interaction=nonstopmode -output-directory=%o %f"))
Org LaTeX packages
;; 'svg' package depends on inkscape, imagemagik and ghostscript
(when (+all (mapcar 'executable-find '("inkscape" "magick" "gs")))
(add-to-list 'org-latex-packages-alist '("" "svg")))
(add-to-list 'org-latex-packages-alist '("svgnames" "xcolor"))
;; (add-to-list 'org-latex-packages-alist '("" "fontspec")) ;; for xelatex
;; (add-to-list 'org-latex-packages-alist '("utf8" "inputenc"))
Export PDFs with syntax highlighting
This is for code syntax highlighting in export. You need to use -shell-escape
with latex, and install the python-pygments
package.
;; Should be configured per document, as a local variable
;; (setq org-latex-listings 'minted)
;; (add-to-list 'org-latex-packages-alist '("" "minted"))
;; Default `minted` options, can be overwritten in file/dir locals
(setq org-latex-minted-options
'(("frame" "lines")
("fontsize" "\\footnotesize")
("tabsize" "2")
("breaklines" "true")
("breakanywhere" "true") ;; break anywhere, no just on spaces
("style" "default")
("bgcolor" "GhostWhite")
("linenos" "true")))
;; Link some org-mode blocks languages to lexers supported by minted
;; via (pygmentize), you can see supported lexers by running this command
;; in a terminal: `pygmentize -L lexers'
(dolist (pair '((ipython "python")
(jupyter "python")
(scheme "scheme")
(lisp-data "lisp")
(conf-unix "unixconfig")
(conf-space "unixconfig")
(authinfo "unixconfig")
(gdb-script "unixconfig")
(conf-toml "yaml")
(conf "ini")
(gitconfig "ini")
(systemd "ini")))
(unless (member pair org-latex-minted-langs)
(add-to-list 'org-latex-minted-langs pair)))
Class templates
(after! ox-latex
(add-to-list
'org-latex-classes
'("scr-article"
"\\documentclass{scrartcl}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("lettre"
"\\documentclass{lettre}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("blank"
"[NO-DEFAULT-PACKAGES]\n[NO-PACKAGES]\n[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("IEEEtran"
"\\documentclass{IEEEtran}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("ieeeconf"
"\\documentclass{ieeeconf}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("sagej"
"\\documentclass{sagej}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list
'org-latex-classes
'("thesis"
"\\documentclass[11pt]{book}"
("\\chapter{%s}" . "\\chapter*{%s}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")))
(add-to-list
'org-latex-classes
'("thesis-fr"
"\\documentclass[french,12pt,a4paper]{book}"
("\\chapter{%s}" . "\\chapter*{%s}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}"))))
(setq org-latex-default-class "article")
;; org-latex-tables-booktabs t
;; org-latex-reference-command "\\cref{%s}")
Export multi-files Org documents
Let’s say we have a multi-files document, with main.org
as the entry point.
Supposing a document with a structure like this:
Files intro.org
, chap1.org
, … are included in main.org
using the Org command
. In such a setup, we will spend most of our time
writing in a chapter files, and not the main.org
, where when want to export the
document, we would need to open the top-level file main.org
before exporting.
A quick solution is to admit the following convention:
If a file named
main.org
is present beside any other Org file, it should be considered as the entry point; and whenever we export to PDF (from any of the Org files like:intro.org
,chap1.org
, …), we automatically jump to themain.org
, and run the export there.
This can be achieved by adding an Emacs-Lisp advice around the
(org-latex-export-to-pdf)
to switch to main.org
(if it exists) before
running the export.
You can also set the variable +org-export-to-pdf-main-file
to the main file, in
.dir-locals.el
or as a file local variable.
(defvar +org-export-to-pdf-main-file nil
"The main (entry point) Org file for a multi-files document.")
(advice-add
'org-latex-export-to-pdf :around
(lambda (orig-fn &rest orig-args)
(message
"PDF exported to: %s."
(let ((main-file (or (bound-and-true-p +org-export-to-pdf-main-file) "main.org")))
(if (file-exists-p (expand-file-name main-file))
(with-current-buffer (find-file-noselect main-file)
(apply orig-fn orig-args))
(apply orig-fn orig-args))))))
Hugo
Update files with last modified date, when #+lastmod:
is available
(setq time-stamp-active t
time-stamp-start "#\\+lastmod:[ \t]*"
time-stamp-end "$"
time-stamp-format "%04Y-%02m-%02d")
(add-hook 'before-save-hook 'time-stamp nil)
(setq org-hugo-auto-set-lastmod t)
Text editing
Plain text
It’s nice to see ANSI color codes displayed. However, until Emacs 28 it’s not possible to do this without modifying the buffer, so let’s condition this block on that.
(after! text-mode
(add-hook! 'text-mode-hook
(unless (derived-mode-p 'org-mode)
;; Apply ANSI color codes
(with-silent-modifications
(ansi-color-apply-on-region (point-min) (point-max) t)))))
Academic phrases
When writing your academic paper, you might get stuck trying to find the right phrase that captures your intention. This package tries to alleviate that problem by presenting you with a list of phrases organized by the topic or by the paper section that you are writing. This package has around 600 phrases so far.
This is based on the book titled “English for Writing Research - Papers Useful Phrases”.
(use-package! academic-phrases
:commands (academic-phrases
academic-phrases-by-section))
French apostrophes
(defun +helper--in-buffer-replace (old new)
"Replace OLD with NEW in the current buffer."
(save-excursion
(goto-char (point-min))
(let ((case-fold-search nil)
(cnt 0))
(while (re-search-forward old nil t)
(replace-match new)
(setq cnt (1+ cnt)))
cnt)))
(defun +helper-clear-frenchy-ponctuations ()
"Replace french apostrophes (’) by regular quotes (')."
(interactive)
(let ((chars '((" " . "") ("’" . "'")))
(cnt 0))
(dolist (pair chars)
(setq cnt (+ cnt (+helper--in-buffer-replace (car pair) (cdr pair)))))
(message "Replaced %d matche(s)." cnt)))
Yanking multi-lines paragraphs
(defun +helper-paragraphized-yank ()
"Copy, then remove newlines and Org styling (/*_~)."
(interactive)
(copy-region-as-kill nil nil t)
(with-temp-buffer
(yank)
;; Remove newlines, and Org styling (/*_~)
(goto-char (point-min))
(let ((case-fold-search nil))
(while (re-search-forward "[\n/*_~]" nil t)
(replace-match (if (s-matches-p (match-string 0) "\n") " " "") t)))
(kill-region (point-min) (point-max))))
(map! :localleader
:map (org-mode-map markdown-mode-map latex-mode-map text-mode-map)
:desc "Paragraphized yank" "y" #'+helper-paragraphized-yank)
System configuration
Mime types
Org mode files
Org mode isn’t recognized as its own mime type by default, but that can easily
be changed with the following file. For system-wide changes try
/usr/share/mime/packages/org.xml
.
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
<mime-type type="text/org">
<comment>Emacs Org-mode File</comment>
<glob pattern="*.org"/>
<alias type="text/org"/>
</mime-type>
</mime-info>
What’s nice is that Papirus now has an icon for text/org
. One simply needs to
refresh their mime database:
update-mime-database ~/.local/share/mime
Then set Emacs as the default editor:
xdg-mime default emacs-client.desktop text/org
Registering org-protocol://
The recommended method of registering a protocol is by registering a desktop application, which seems reasonable.
[Desktop Entry]
Name=Emacs Org-Protocol
Exec=emacsclient %u
Icon=/home/hacko/.doom.d/assets/org-mode.svg
Type=Application
Terminal=false
MimeType=x-scheme-handler/org-protocol
To associate org-protocol://
links with the desktop file:
xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol
Configuring Chrome/Brave
As specified in the official documentation, we would like to invoke the
org-protocol://
without confirmation. To do this, we need to add this system-wide configuration.
read -p "Do you want to set Chrome/Brave to show the 'Always open ...' checkbox, to be used with the 'org-protocol://' registration? [Y | N]: " INSTALL_CONFIRM
if [[ "$INSTALL_CONFIRM" == "Y" ]]
then
sudo mkdir -p /etc/opt/chrome/policies/managed/
sudo tee /etc/opt/chrome/policies/managed/external_protocol_dialog.json > /dev/null <<'EOF'
{
"ExternalProtocolDialogShowAlwaysOpenCheckbox": true
}
EOF
sudo chmod 644 /etc/opt/chrome/policies/managed/external_protocol_dialog.json
fi
Then add a bookmarklet in your browser with this code:
javascript:location.href =
'org-protocol://roam-ref?template=r&ref='
+ encodeURIComponent(location.href)
+ '&title='
+ encodeURIComponent(document.title)
+ '&body='
+ encodeURIComponent(window.getSelection())
Git
Git diffs
Based on this gist and this article.
*.tex diff=tex
*.bib diff=bibtex
*.{c,h,c++,h++,cc,hh,cpp,hpp} diff=cpp
*.m diff=matlab
*.py diff=python
*.rb diff=ruby
*.php diff=php
*.pl diff=perl
*.{html,xhtml} diff=html
*.f diff=fortran
*.{el,lisp,scm} diff=lisp
*.r diff=rstats
*.texi* diff=texinfo
*.org diff=org
*.rs diff=rust
*.odt diff=odt
*.odp diff=libreoffice
*.ods diff=libreoffice
*.doc diff=doc
*.xls diff=xls
*.ppt diff=ppt
*.docx diff=docx
*.xlsx diff=xlsx
*.pptx diff=pptx
*.rtf diff=rtf
*.{png,jpg,jpeg,gif} diff=exif
*.pdf diff=pdf
*.djvu diff=djvu
*.epub diff=pandoc
*.chm diff=tika
*.mhtml? diff=tika
*.{class,jar} diff=tika
*.{rar,7z,zip,apk} diff=tika
Then adding some regular expressions for it to ~/.config/git/config
, with some
tools to view diffs on binary files.
# ====== TEXT FORMATS ======
[diff "org"]
xfuncname = "^(\\*+ +.*)$"
[diff "lisp"]
xfuncname = "^(\\(.*)$"
[diff "rstats"]
xfuncname = "^([a-zA-z.]+ <- function.*)$"
[diff "texinfo"]
# from http://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;f=.gitattributes;h=c3b2926c78c939d94358cc63d051a70d38cfea5d;hb=HEAD
xfuncname = "^@node[ \t][ \t]*\\([^,][^,]*\\)"
[diff "rust"]
xfuncname = "^[ \t]*(pub|)[ \t]*((fn|struct|enum|impl|trait|mod)[^;]*)$"
# ====== BINARY FORMATS ======
[diff "pdf"]
binary = true
# textconv = pdfinfo
# textconv = sh -c 'pdftotext "$@" -' # sudo apt install pdftotext
textconv = sh -c 'pdftotext -layout "$0" -enc UTF-8 -nopgbrk -q -'
cachetextconv = true
[diff "djvu"]
binary = true
# textconv = pdfinfo
textconv = djvutxt # yay -S djvulibre
cachetextconv = true
[diff "odt"]
textconv = odt2txt
# textconv = pandoc --standalone --from=odt --to=plain
binary = true
cachetextconv = true
[diff "doc"]
# textconv = wvText
textconv = catdoc # yay -S catdoc
binary = true
cachetextconv = true
[diff "xls"]
# textconv = in2csv
# textconv = xlscat -a UTF-8
# textconv = soffice --headless --convert-to csv
textconv = xls2csv # yay -S catdoc
binary = true
cachetextconv = true
[diff "ppt"]
textconv = catppt # yay -S catdoc
binary = true
cachetextconv = true
[diff "docx"]
textconv = pandoc --standalone --from=docx --to=plain
# textconv = sh -c 'docx2txt.pl "$0" -'
binary = true
cachetextconv = true
[diff "xlsx"]
textconv = xlsx2csv # pip install xlsx2csv
# textconv = in2csv
# textconv = soffice --headless --convert-to csv
binary = true
cachetextconv = true
[diff "pptx"]
# pip install --user pptx2md (currently not wotking with Python 3.10)
# textconv = sh -c 'pptx2md --disable_image --disable_wmf -i "$0" -o ~/.cache/git/presentation.md >/dev/null && cat ~/.cache/git/presentation.md'
# Alternative hack, convert PPTX to PPT, then use the catppt tool
textconv = sh -c 'soffice --headless --convert-to ppt --outdir /tmp "$0" && TMP_FILENAME=$(basename -- "$0") && catppt "/tmp/${TMP_FILENAME%.*}.ppt"'
binary = true
cachetextconv = true
[diff "rtf"]
textconv = unrtf --text # yay -S unrtf
binary = true
cachetextconv = true
[diff "epub"]
textconv = pandoc --standalone --from=epub --to=plain
binary = true
cachetextconv = true
[diff "tika"]
textconv = tika --config=~/.local/share/tika/tika-conf.xml --text
binary = true
cachetextconv = true
[diff "libreoffice"]
textconv = soffice --cat
binary = true
cachetextconv = true
[diff "exif"]
binary = true
textconv = exiftool # sudo apt install perl-image-exiftool
Apache Tika App wrapper
Apache Tika is a content detection and analysis framework. It detects and extracts metadata and text from over a thousand different file types. We will be using the Tika App in command-line mode to show some meaningful diff information for some binary files.
First, let’s add a custom script to run tika-app
:
#!/bin/sh
APACHE_TIKA_JAR="$HOME/.local/share/tika/tika-app.jar"
if [ -f "${APACHE_TIKA_JAR}" ]
then
exec java -Dfile.encoding=UTF-8 -jar "${APACHE_TIKA_JAR}" "$@" 2>/dev/null
else
echo "JAR file not found at ${APACHE_TIKA_JAR}"
fi
Add tika
’s installation instructions to the setup.sh
file.
update_apache_tika () {
TIKA_JAR_PATH="$HOME/.local/share/tika"
if [ ! -d "${TIKA_JAR_PATH}" ]
then
mkdir -p "${TIKA_JAR_PATH}"
fi
TIKA_BASE_URL=https://archive.apache.org/dist/tika/
TIKA_JAR_LINK="${TIKA_JAR_PATH}/tika-app.jar"
echo -n "Checking for new Apache Tika App version... "
# Get the lastest version
TIKA_VERSION=$(
curl -s "${TIKA_BASE_URL}" | # Get the page
pandoc -f html -t plain | # Convert HTML page to plain text.
awk '/([0-9]+\.)+[0-1]\// {print substr($1, 0, length($1)-1)}' | # Get the versions directories (pattern: X.X.X/)
sort -rV | # Sort versions, the newest first
head -n 1 # Get the first (newest) version
)
if [ -z "${TIKA_VERSION}" ]
then
echo "Failed, check your internet connection."
exit 1
fi
echo "Lastest version is ${TIKA_VERSION}"
TIKA_JAR="${TIKA_JAR_PATH}/tika-app-${TIKA_VERSION}.jar"
TIKA_JAR_URL="${TIKA_BASE_URL}${TIKA_VERSION}/tika-app-${TIKA_VERSION}.jar"
if [ ! -f "${TIKA_JAR}" ]
then
echo "New version available!"
read -p "Do you want to download Apache Tika App v${TIKA_VERSION}? [Y | N]: " INSTALL_CONFIRM
if [[ "$INSTALL_CONFIRM" == "Y" ]]
then
curl -o "${TIKA_JAR}" "${TIKA_JAR_URL}" && echo "Apache Tika App v${TIKA_VERSION} downloaded successfully"
fi
else
echo "Apache Tika App is up-to-date, version ${TIKA_VERSION} already downloaded to '${TIKA_JAR}'"
fi
# Check the existance of the symbolic link
if [ -L "${TIKA_JAR_LINK}" ]
then
unlink "${TIKA_JAR_LINK}"
fi
# Create a symbolic link to the installed version
ln -s "${TIKA_JAR}" "${TIKA_JAR_LINK}"
}
update_apache_tika;
When it detects that Tesseract is installed, Tika App will try to extract text
from some file types. For some reason, it tries to use Tesseract with some
compressed files like *.bz2
, *.apk
… etc. I would like to disable this feature
by exporting an XML config file which will be used when launching the Tika App
(using --config=<tika-config.xml>
).
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<parsers>
<parser class="org.apache.tika.parser.DefaultParser">
<parser-exclude class="org.apache.tika.parser.ocr.TesseractOCRParser"/>
</parser>
</parsers>
</properties>
Emacs’ Systemd daemon
Let’s define a Systemd service to launch Emacs server automatically.
[Unit]
Description=Emacs server daemon
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
[Service]
Type=forking
ExecStart=sh -c 'emacs --daemon && emacsclient -c --eval "(delete-frame)"'
ExecStop=emacsclient --no-wait --eval "(progn (setq kill-emacs-hook nil) (kill-emacs))"
Restart=on-failure
[Install]
WantedBy=default.target
Which is then enabled by:
systemctl --user enable emacs.service
For some reason if a frame isn’t opened early in the initialization process, the
daemon doesn’t seem to like opening frames later — hence the && emacsclient -c --eval "(delete-frame)"
part of the ExecStart
value.
Emacs client
Desktop integration
It can now be nice to use this as a ‘default app’ for opening files. If we add an appropriate desktop entry, and enable it in the desktop environment.
[Desktop Entry]
Name=Emacs (Client)
GenericName=Text Editor
Comment=A flexible platform for end-user applications
MimeType=text/english;text/plain;text/org;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Exec=emacsclient -create-frame --frame-parameters="'(fullscreen . maximized)" --alternate-editor="/usr/bin/emacs -mm" --no-wait %F
Icon=emacs
Type=Application
Terminal=false
Categories=TextEditor;Utility;
StartupWMClass=Emacs
Keywords=Text;Editor;
X-KDE-StartupNotify=false
[Desktop Entry]
Name=Emacs
GenericName=Text Editor
Comment=A flexible platform for end-user applications
MimeType=text/english;text/plain;text/org;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Exec=emacs -mm %F
Icon=emacs
Type=Application
Terminal=false
Categories=TextEditor;Utility;
StartupWMClass=Emacs
Keywords=Text;Editor;
X-KDE-StartupNotify=false
Command-line wrapper
A wrapper around emacsclient
:
- Accepting
stdin
by putting it in a temporary file and immediately opening it. - Guessing that the
tty
is a good idea when$DISPLAY
is unset (relevant with SSH sessions, among other things). - With a whiff of 24-bit color support, sets
TERM
variable to aterminfo
that (probably) announces 24-bit color support. - Changes GUI
emacsclient
instances to be non-blocking by default (--no-wait
), and instead take a flag to suppress this behavior (-w
).
I would use sh
, but using arrays for argument manipulation is just too
convenient, so I’ll raise the requirement to bash
. Since arrays are the only
’extra’ compared to sh
, other shells like ksh
etc. should work too.
#!/usr/bin/env bash
force_tty=false
force_wait=false
stdin_mode=""
args=()
usage () {
echo -e "Usage: e [-t] [-m MODE] [OPTIONS] FILE [-]
Emacs client convenience wrapper.
Options:
-h, --help Show this message
-t, -nw, --tty Force terminal mode
-w, --wait Don't supply --no-wait to graphical emacsclient
- Take stdin (when last argument)
-m MODE, --mode MODE Mode to open stdin with
-mm, --maximized Start Emacs client in maximized window
Run emacsclient --help to see help for the emacsclient."
}
while :
do
case "$1" in
-t | -nw | --tty)
force_tty=true
shift ;;
-w | --wait)
force_wait=true
shift ;;
-m | --mode)
stdin_mode=" ($2-mode)"
shift 2 ;;
-mm | --maximized)
args+=("--frame-parameters='(fullscreen . maximized)")
shift ;;
-h | --help)
usage
exit 0 ;;
--*=*)
set -- "$@" "${1%%=*}" "${1#*=}"
shift ;;
*)
[ "$#" = 0 ] && break
args+=("$1")
shift ;;
esac
done
if [ ! "${#args[*]}" = 0 ] && [ "${args[-1]}" = "-" ]
then
unset 'args[-1]'
TMP="$(mktemp /tmp/emacsstdin-XXX)"
cat > "$TMP"
args+=(--eval "(let ((b (generate-new-buffer \"*stdin*\"))) (switch-to-buffer b) (insert-file-contents \"$TMP\") (delete-file \"$TMP\")${stdin_mode})")
fi
if [ -z "$DISPLAY" ] || $force_tty
then
# detect terminals with sneaky 24-bit support
if { [ "$COLORTERM" = truecolor ] || [ "$COLORTERM" = 24bit ]; } \
&& [ "$(tput colors 2>/dev/null)" -lt 257 ]
then
if echo "$TERM" | grep -q "^\w\+-[0-9]"
then
termstub="${TERM%%-*}"
else
termstub="${TERM#*-}"
fi
if infocmp "$termstub-direct" >/dev/null 2>&1
then
TERM="$termstub-direct"
else
TERM="xterm-direct"
fi # should be fairly safe
fi
emacsclient --tty -create-frame --alternate-editor="/usr/bin/emacs" "${args[@]}"
else
if ! $force_wait
then
args+=(--no-wait)
fi
emacsclient -create-frame --alternate-editor="/usr/bin/emacs" "${args[@]}"
fi
Useful aliases
Now, to set an alias to use e
with magit
, and then for maximum laziness we can
set aliases for the terminal-forced variants.
# Aliases to run emacs+magit
alias magit='e --eval "(progn (magit-status) (delete-other-windows))"'
alias magitt='e -t --eval "(progn (magit-status) (delete-other-windows))"'
# Aliases to run emacs+mu4e
alias emu='e --eval "(progn (=mu4e) (delete-other-windows))"'
alias emut='e -t --eval "(progn (=mu4e) (delete-other-windows))"'
And this to launch Emacs in terminal mode et
, I use this as a default $EDITOR
#!/usr/bin/env bash
e -t "$@"
And ev
for use with $VISUAL
:
#!/usr/bin/env bash
e -w "$@"
export EDITOR="$HOME/.local/bin/et"
# export VISUAL=$HOME/.local/bin/ev
AppImage
Install/update the appimageupdatetool.AppImage
tool:
update_appimageupdatetool () {
TOOL_NAME=appimageupdatetool
MACHINE_ARCH=$(uname -m)
APPIMAGE_UPDATE_TOOL_PATH="$HOME/.local/bin/${TOOL_NAME}"
APPIMAGE_UPDATE_TOOL_URL="https://github.com/AppImage/AppImageUpdate/releases/download/continuous/${TOOL_NAME}-${MACHINE_ARCH}.AppImage"
if [ -f "${APPIMAGE_UPDATE_TOOL_PATH}" ] && "$APPIMAGE_UPDATE_TOOL_PATH" -j "${APPIMAGE_UPDATE_TOOL_PATH}" 2&>/dev/null
then
echo "${TOOL_NAME} already up to date"
else
if [ -f "${APPIMAGE_UPDATE_TOOL_PATH}" ]
then
echo "Update available, downloading latest ${MACHINE_ARCH} version to ${APPIMAGE_UPDATE_TOOL_PATH}"
mv "${APPIMAGE_UPDATE_TOOL_PATH}" "${APPIMAGE_UPDATE_TOOL_PATH}.backup"
else
echo "${TOOL_NAME} not found, downloading latest ${MACHINE_ARCH} version to ${APPIMAGE_UPDATE_TOOL_PATH}"
fi
wget -O "${APPIMAGE_UPDATE_TOOL_PATH}" "${APPIMAGE_UPDATE_TOOL_URL}" && # 2&>/dev/null
echo "Downloaded ${TOOL_NAME}-${MACHINE_ARCH}.AppImage" &&
[ -f "${APPIMAGE_UPDATE_TOOL_PATH}.backup" ] &&
rm "${APPIMAGE_UPDATE_TOOL_PATH}.backup"
chmod a+x "${APPIMAGE_UPDATE_TOOL_PATH}"
fi
}
update_appimageupdatetool;
Oh-my-Zsh
Path
Path to your oh-my-zsh installation.
export ZSH="$HOME/.oh-my-zsh"
Themes and customization:
Set name of the theme to load, if set to "random"
, it will load a random theme
each time oh-my-zsh is loaded, in which case, to know which specific one was
loaded, run: echo $RANDOM_THEME
See github.com/ohmyzsh/ohmyzsh/wiki/Themes.
# Typewritten customizations
TYPEWRITTEN_RELATIVE_PATH="adaptive"
TYPEWRITTEN_CURSOR="underscore"
ZSH_THEME="typewritten/typewritten"
# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )
Behavior
# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"
# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"
# Uncomment the following line to disable bi-weekly auto-update checks.
# DISABLE_AUTO_UPDATE="true"
# Uncomment the following line to automatically update without prompting.
DISABLE_UPDATE_PROMPT="true"
# Uncomment the following line to change how often to auto-update (in days).
export UPDATE_ZSH_DAYS=3
# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true"
# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"
# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"
# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"
# Uncomment the following line to display red dots whilst waiting for completion.
# COMPLETION_WAITING_DOTS="true"
# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"
# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"
Plugins
# Would you like to use another custom folder than $ZSH/custom?
ZSH_CUSTOM=$HOME/.config/my_ohmyzsh_customizations
# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(
zsh-autosuggestions
zsh-navigation-tools
zsh-interactive-cd
archlinux
ssh-agent
sudo
docker
systemd
tmux
python
pip
rust
repo
git
cp
rsync
ripgrep
fzf
fd
z
)
Bootstrap Oh-my-Zsh
source $ZSH/oh-my-zsh.sh
Aliases
# Aliases
alias zshconfig="vim ~/.zshrc"
alias ohmyzsh="ranger $ZSH"
Zsh user configuration
pbcopy
and pbpaste
I like to define MacOS-like commands (pbcopy
and pbpaste
) to copy and paste in
terminal (from stdin
, to stdout
). The pbcopy
and pbpaste
are defined using
either xclip
or xsel
, you would need to install these tools, otherwise we wouldn’t
define the aliases.
# Define aliases to 'pbcopy' and 'pbpaste'
if command -v xclip &> /dev/null
then
# Define aliases using xclip
alias pbcopy='xclip -selection clipboard'
alias pbpaste='xclip -selection clipboard -o'
elif command -v xsel &> /dev/null
then
# Define aliases using xsel
alias pbcopy='xsel --clipboard --input'
alias pbpaste='xsel --clipboard --output'
fi
netpaste
Define a netpaste
command to paste to a Pastebin server.
alias netpaste='curl -F file=@- 0x0.st' # OR 'curl -F f:1=<- ix.io '
Sudo GUI!
And then define gsuon
and gsuoff
aliases to run graphical apps from terminal
with root permissions, this requires xhost
.
# To run GUI apps from terminal with root permissions
if command -v xhost &> /dev/null
then
alias gsuon='xhost si:localuser:root'
alias gsuoff='xhost -si:localuser:root'
fi
Neovim
Use Neovim instead of VIM to provide vi
and vim
commands.
# NeoVim
if command -v nvim &> /dev/null
then
alias vim="nvim"
alias vi="nvim"
fi
ESP-IDF
Add some aliases to work with the ESP-IDF framework.
if [ -d "$HOME/Softwares/src/esp-idf/" ]
then
alias esp-prepare-env='source $HOME/Softwares/src/esp-idf/export.sh'
alias esp-update='echo "Updating ESP-IDF framework..." && cd $HOME/src/esp-idf && git pull --all && echo "Updated successfully"'
else
alias esp-prepare-env='echo "esp-idf repo not found. You can clone the esp-idf repo using git clone https://github.com/espressif/esp-idf.git"'
alias esp-update=esp-prepare-env
fi
CLI wttrin client
Define an alias to get weather information for my city:
export WTTRIN_CITY=Orsay
alias wttrin='curl wttr.in/$WTTRIN_CITY'
alias wttrin2='curl v2.wttr.in/$WTTRIN_CITY'
Minicom
Enable Meta key and colors in minicom
:
export MINICOM='-m -c on'
Rust
Define Rust sources path, and add packages installed from cargo
to the PATH
.
export RUST_SRC_PATH=$HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/
export PATH=$PATH:$HOME/.cargo/bin
I’m using the AUR package clang-format-static-bin
, which provide multiple
versions of Clang-format, I use it with some work projects requiring a specific
version of Clang-format.
Clang-format
export PATH=$PATH:/opt/clang-format-static
CMake
Add my manually installed libraries to CMake and PATH
.
export CMAKE_PREFIX_PATH=$HOME/Softwares/src/install
export PATH=$PATH:$HOME/Softwares/src/install/bin
Node
Set NPM installation path to local:
NPM_PACKAGES="${HOME}/.npm-packages"
# Export NPM bin path
export PATH="$PATH:$NPM_PACKAGES/bin"
# Preserve MANPATH if you already defined it somewhere in your config.
# Otherwise, fall back to `manpath` so we can inherit from `/etc/manpath`.
export MANPATH="${MANPATH-$(manpath)}:$NPM_PACKAGES/share/man"
# Tell Node about these packages
export NODE_PATH="$NPM_PACKAGES/lib/node_modules:$NODE_PATH"
Tell NPM to use this directory for its global package installs by adding this
in ~/.npmrc
:
prefix = ~/.npm-packages
Some useful stuff (fzf
, opam
, Doom Emacs…)
tmux
I like to use tmux
by default, even on my local sessions, I like to start a tmux
in a default
session on the first time I launch a terminal, and then, attach any
other terminal to this default session:
# If not running inside Emacs (via vterm/eshell...)
if [ -z $INSIDE_EMACS ]
then
if command -v tmux &> /dev/null && [ -z "$TMUX" ]
then
tmux attach -t default || tmux new -s default
fi
fi
Other stuff
# You may need to manually set your language environment
# export LANG=en_US.UTF-8
# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
# export EDITOR='vim'
# else
# export EDITOR='mvim'
# fi
# Compilation flags
# export ARCHFLAGS="-arch x86_64"
# FZF
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# OPAM configuration
[[ ! -r $HOME/.opam/opam-init/init.zsh ]] || source $HOME/.opam/opam-init/init.zsh > /dev/null 2> /dev/null
# Add ~/.config/emacs/bin to path (for DOOM Emacs stuff)
export PATH=$PATH:$HOME/.config/emacs/bin
export TEXMFHOME=$HOME/.texmf
Define some environment variables.
export DS_DIR=~/PhD/datasets-no/experiment_images/
export DSO_BIN_DIR=~/PhD/workspace-no/vo/orig/dso/build/release/bin
export DSO_RES_DIR=~/PhD/workspace-no/vo/orig/dso_results
Load my bitwarden-cli
session, exported to BW_SESSION
.
source ~/.bitwarden-session
Rust format
For Rust code base, the file $HOME/.rustfmt.toml
contains the global format
settings, I like to set it to:
# Rust edition 2018
edition = "2018"
# Use Unix style newlines, with 2 spaces tabulation.
newline_style = "Unix"
tab_spaces = 2
hard_tabs = false
# Make one line functions in a single line
fn_single_line = true
# Format strings
format_strings = true
# Increase the max line width
max_width = 120
# Merge nested imports
merge_imports = true
# Enum and Struct alignement
enum_discrim_align_threshold = 20
struct_field_align_threshold = 20
# Reorder impl items: type > const > macros > methods.
reorder_impl_items = true
# Comments and documentation formating
wrap_comments = true
normalize_comments = true
normalize_doc_attributes = true
format_code_in_doc_comments = true
report_fixme = "Always"
todo = "Always"
eCryptfs
Unlock and mount script
#!/bin/sh -e
# This script mounts a user's confidential private folder
#
# Original by Michael Halcrow, IBM
# Extracted to a stand-alone script by Dustin Kirkland <kirkland@ubuntu.com>
# Modified by: Abdelhak Bougouffa <abougouffa@fedoraproject.org>
#
# This script:
# * interactively prompts for a user's wrapping passphrase (defaults to their
# login passphrase)
# * checks it for validity
# * unwraps a users mount passphrase with their supplied wrapping passphrase
# * inserts the mount passphrase into the keyring
# * and mounts a user's encrypted private folder
PRIVATE_DIR="Private"
PW_ATTEMPTS=3
MESSAGE=`gettext "Enter your login passphrase:"`
if [ -f $HOME/.ecryptfs/wrapping-independent ]
then
# use a wrapping passphrase different from the login passphrase
MESSAGE=`gettext "Enter your wrapping passphrase:"`
fi
WRAPPED_PASSPHRASE_FILE="$HOME/.ecryptfs/wrapped-passphrase"
MOUNT_PASSPHRASE_SIG_FILE="$HOME/.ecryptfs/$PRIVATE_DIR.sig"
# First, silently try to perform the mount, which would succeed if the appropriate
# key is available in the keyring
if /sbin/mount.ecryptfs_private >/dev/null 2>&1
then
exit 0
fi
# Otherwise, interactively prompt for the user's password
if [ -f "$WRAPPED_PASSPHRASE_FILE" -a -f "$MOUNT_PASSPHRASE_SIG_FILE" ]
then
tries=0
while [ $tries -lt $PW_ATTEMPTS ]
do
LOGINPASS=`zenity --password --title "eCryptFS: $MESSAGE"`
if [ $(wc -l < "$MOUNT_PASSPHRASE_SIG_FILE") = "1" ]
then
# No filename encryption; only insert fek
if printf "%s\0" "$LOGINPASS" | ecryptfs-unwrap-passphrase "$WRAPPED_PASSPHRASE_FILE" - | ecryptfs-add-passphrase -
then
break
else
zenity --error --title "eCryptfs" --text "Error: Your passphrase is incorrect"
tries=$(($tries + 1))
continue
fi
else
if printf "%s\0" "$LOGINPASS" | ecryptfs-insert-wrapped-passphrase-into-keyring "$WRAPPED_PASSPHRASE_FILE" -
then
break
else
zenity --error --title "eCryptfs" --text "Error: Your passphrase is incorrect"
tries=$(($tries + 1))
continue
fi
fi
done
if [ $tries -ge $PW_ATTEMPTS ]
then
zenity --error --title "eCryptfs" --text "Too many incorrect password attempts, exiting"
exit 1
fi
/sbin/mount.ecryptfs_private
else
zenity --error --title "eCryptfs" --text "Encrypted private directory is not setup properly"
exit 1
fi
if grep -qs "$HOME/.Private $PWD ecryptfs " /proc/mounts 2>/dev/null; then
zenity --info --title "eCryptfs" --text "Your private directory has been mounted."
fi
dolphin "$HOME/Private"
exit 0
Desktop integration
[Desktop Entry]
Type=Application
Version=1.0
Name=eCryptfs Unlock Private Directory
Icon=unlock
Exec=/home/hacko/.ecryptfs/ecryptfs-mount-private-gui
Terminal=False
GDB
Early init
I like to disable the initial message (containing copyright info and other
stuff), the right way to do this is either by starting gdb
with -q
option, or
(since GDB v11 I think), by setting in
~/.gdbearlyinit
.
# GDB early init file
# Abdelhak Bougouffa (c) 2022
# Disable showing the initial message
set startup-quietly
Init
GDB loads $HOME/.gdbinit
at startup, I like to define some default options in
this file, this is a WIP, but it won’t evolve too much, as it is recommended to
keep the .gdbinit
clean and simple. For the moment, it does just enable pretty
printing, and defines the c
and n
commands to wrap continue
and next
with a post
refresh
, which is helpful with the annoying TUI when the program outputs to the
stdout.
# GDB init file
# Abdelhak Bougouffa (c) 2022
# Save history
set history save on
set history filename ~/.gdb_history
set history remove-duplicates 2048
# When debugging my apps, debug information of system libraries
# aren't that important
set debuginfod enabled off
# Set pretty print
set print pretty on
# I hate stepping into system libraries when I'm debugging my
# crappy stuff, so lets add system headers to skipped files
skip pending on
python
import os
# Add paths here, they will be explored recursivly
LIB_PATHS = ["/usr/include" "/usr/local/include"]
for lib_path in LIB_PATHS:
for root, dirs, files in os.walk(lib_path):
for file in files:
cmd = f"skip file {os.path.join(root, file)}"
gdb.execute(cmd, True, to_string=True)
end
skip enable
skip pending on
guile
<<gdb-init-guile>>
end
skip enable
# This fixes the annoying ncurses TUI gliches and saves typing C-l each time to refresh the screen
define cc
continue
refresh
end
define nn
next
refresh
end
GnuPG
I add this to my ~/.gnupg/gpg-agent.conf
, to set the time-to-live to one day.
# Do not ask me about entered passwords for 24h (during the same session)
default-cache-ttl 86400
max-cache-ttl 86400
# As I'm using KDE, use Qt based pinentry tool instead of default GTK+
pinentry-program /usr/bin/pinentry-qt
# Allow pinentry in Emacs minibuffer (combined with epg-pinentry-mode)
allow-loopback-pinentry
allow-emacs-pinentry
OCR This
#!/bin/bash
IMG=$(mktemp -u --suffix=".png")
scrot -s "$IMG" -q 100
mogrify -modulate 100,0 -resize 400% "$IMG"
tesseract "$IMG" - -l eng 2> /dev/null | xsel -ib
Slack
This script is called at system startup.
#!/bin/bash
WEEK_DAY=$(date +%u)
HOUR=$(date +%H)
SLACK=$(which slack)
if [ ! "$WEEK_DAY" == "6" ] && [ ! "$WEEK_DAY" == "7" ] && [ "$HOUR" -gt 7 ] && [ "$HOUR" -lt 20 ] ; then
$SLACK -u %U
else
echo "It is not work time!"
fi
Arch Linux packages
Here, we install Arch packages
check_and_install_pkg() {
PKG_NAME="$1"
if ! pacman -Qiq "${PKG_NAME}" &>/dev/null; then
echo "Package ${PKG_NAME} is missing, installing it using yay"
yay -S "${PKG_NAME}"
fi
}
PKGS_LIST=(
git ripgrep fd gnupg fzf the_silver_searcher
ttf-ibm-plex ttf-fira-code ttf-roboto-mono ttf-overpass ttf-lato ttf-input
ttf-cascadia-code ttf-jetbrains-mono ttf-fantasque-sans-mono
ttc-iosevka ttf-iosevka-nerd ttc-iosevka-slab ttc-iosevka-curly
ttc-iosevka-curly-slab ttc-iosevka-etoile ttc-iosevka-ss09
ccls cppcheck clang gcc gdb lldb valgrind rr openocd
sbcl cmucl clisp chez-scheme mit-scheme chibi-scheme chicken
vls vlang rustup semgrep-bin
mu isync msmtp xsel xorg-xhost
mpc mpv mpd vlc yt-dlp
maxima fricas octave scilab-bin graphviz jupyterlab jupyter-notebook r
djvulibre catdoc unrtf perl-image-exiftool wkhtmltopdf
chezmoi neovim repo ecryptfs-utils
pandoc hugo inkscape imagemagick
aspell aspell-en aspell-fr aspell-ar grammalecte language-tool ltex-ls-bin
libvterm brave zotero bitwarden-cli binutils
poppler ffmpegthumbnailer mediainfo imagemagick tar unzip
)
for PKG in "${PKGS_LIST[@]}"; do
check_and_install_pkg "$PKG"
done
KDE Plasma
On KDE, there is a good support for HiDPI displays, however, I faced annoying
small icons in some contexts (for example, a right click on desktop). This can
be fixed by setting PLASMA_USE_QT_SCALING=1
before starting KDE Plasma. KDE
sources the files with .sh
extension found on ~/.config/plasma-workspace/env
, so
let’s create ours.
export PLASMA_USE_QT_SCALING=1