emacs-config/config.org

6675 lines
281 KiB
Org Mode
Raw Normal View History

# -*- mode: org; eval: (add-hook 'after-save-hook (lambda () (org-html-export-to-html t)) nil t) -*-
#+TITLE: Doom Emacs Configuration
2020-07-16 20:45:26 +00:00
#+SUBTITLE: The Methods, Management, and Menagerie of Madness
#+AUTHOR: tecosaur
#+PROPERTY: header-args:emacs-lisp :tangle yes :cache yes :results silent :comments link
#+PROPERTY: header-args:shell :tangle "setup.sh"
#+PROPERTY: header-args :tangle no :results silent
#+HTML_HEAD: <link rel='shortcut icon' type='image/png' href='https://www.gnu.org/software/emacs/favicon.png'>
2020-07-22 04:25:48 +00:00
#+BEGIN_EXPORT html
<a href="https://github.com/tecosaur/emacs-config/"
style="font-family: 'Open Sans'; background-image: none; color: inherit;
text-decoration: none; position: relative; top: clamp(-26px, calc(1280px - 100vw), 0px); opacity: 0.7;">
2020-07-22 04:25:48 +00:00
<img src="https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg"
2020-08-29 15:32:00 +00:00
class="invertible" alt="GitHub Octicon"
2020-07-22 04:25:48 +00:00
style="height: 1em; position: relative; top: 0.1em;">
View on GitHub</a>
#+END_EXPORT
#+BEGIN_EXPORT latex
\newpage % because the contents are multi-page, this looks better
#+END_EXPORT
2020-07-22 04:25:48 +00:00
#+BEGIN_QUOTE
Let us change our traditional attitude to the construction of programs:
Instead of imagining that our main task is to instruct a computer what to do,
let us concentrate rather on explaining to human beings what we want a
computer to do. --- Donald Knuth
#+END_QUOTE
2020-01-20 18:08:07 +00:00
* Intro
Customising an editor can be very rewarding ... until you have to leave it.
For years I have been looking for ways to avoid this pain.
Then I discovered [[https://github.com/cknadler/vim-anywhere][vim-anywhere]], and found that it had an Emacs companion,
[[https://github.com/zachcurry/emacs-anywhere][emacs-anywhere]]. To me, this looked most attractive.
2020-01-20 18:08:07 +00:00
Separately, online I have seen the following statement enough times I think it's a catchphrase
#+BEGIN_QUOTE
2020-05-30 08:09:45 +00:00
Redditor1: I just discovered this thing, isn't it cool. \\
Redditor2: Oh, there's an Emacs mode for that.
2020-01-20 18:08:07 +00:00
#+END_QUOTE
I tried out the =spacemacs= distribution a bit, but it wasn't quite to my liking.
Then I heard about =doom emacs= and thought I may as well give that a try.
TLDR; it's great.
Now I've discovered the wonders of literate programming, and am becoming more
2020-05-30 08:09:45 +00:00
settled by the day. This is my config, and also a cautionary tale (just replace
"Linux" with "Emacs" in the comic below).
[[xkcd:456]]
** Why Emacs?
# https://github.com/esac-io/eos
# https://github.com/remacs/remacs
# https://www.eigenbahn.com/2020/01/12/emacs-is-no-editor
Emacs is not a text editor, this is a common misnomer. It is far more apt to
describe Emacs as Lisp machine providing a generic user-centric text
manipulation environment.
In simpler terms one can think of Emacs as a platform for text-related
applications. It's a vague and generic definition because Emacs itself is
generic.
Good with text. How far does that go? A lot further than one initially thinks:
+ [[https://orgmode.org/][Task planning]]
+ [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html][File management]]
+ [[https://github.com/akermu/emacs-libvterm][Terminal emulation]]
+ [[https://www.djcbsoftware.nl/code/mu/mu4e.html][Email client]]
+ [[https://www.gnu.org/software/tramp/][Remote server tool]]
+ [[https://magit.vc/][Git frontend]]
+ Web [[https://github.com/pashky/restclient.el][client]]/[[https://github.com/skeeto/emacs-web-server][server]]
+ and more...
Ideally, one may use Emacs as /the/ interface to perform =input → transform →
output= cycles, i.e. form a bridge between the human mind and information
manipulation.
*** The enveloping editor
Emacs allows one to do more in one place than any other application. Why is this
good?
+ Enables one to complete tasks with a consistent, standard set of keybindings,
GUI and editing methods --- learn once, use everywhere
+ Reduced context-switching
+ Compressing the stages of a project --- a more centralised workflow can progress
with greater ease
+ Integration between tasks previously relegated to different applications, but
with a common subject --- e.g. linking to an email in a to-do list
*** Some notably unique features
+ Recursive editing
+ Completely introspectable, with pervasive docstrings
+ Mutable environment, which can be incrementally modified
+ Functionality without applications
+ Client-server seperation allows for a daemon, giving near-instant perceived
startup time.
*** Issues
+ Emacs has irritating quirks
+ Some aspects are showing their age (naming conventions, APIs)
+ Emacs is ([[https://www.gnu.org/software/emacs/manual/html_node/elisp/Threads.html][mostly]]) single-threaded, meaning that when something holds that
thread up the whole application freezes
+ A few other nuisances
*** Teach a man to fish...
#+BEGIN_QUOTE
Give a man a fish, and you feed him for a day. Teach a man to fish, and you feed
him for a lifetime. --- Anne Isabella
#+END_QUOTE
Most popular editors have a simple and pretty [[https://code.visualstudio.com/docs/getstarted/settings][settings interface]], filled with
check-boxes, selects, and the occasional text-box. This makes it easy for the
user to pick between common desirable behaviours. To me this is now like /giving
a man a fish/.
What if you want one of those 'check-box' settings to be only on in certain
conditions? Some editors have workspace settings, but that requires you to
manually set the value for /every single instance/. Urgh, [[https://github.com/microsoft/vscode/issues/93153][what]] [[https://github.com/microsoft/vscode/issues/93628][a]] [[https://github.com/microsoft/vscode/issues/5595][pain]].
What if you could set the value of that 'check-box' setting to be the result of
an arbitrary expression evaluated for each file? This is where an editor like
Emacs comes in.
Configuration for Emacs isn't a list of settings in JSON etc. it's *an executable
program which modifies the behaviour of the editor to suit your liking*.
This is 'teaching a man to fish'.
Emacs is built in the same language you configure it in (Emacs [[https://en.wikipedia.org/wiki/Lisp_(programming_language)][Lisp]], or [[https://www.gnu.org/software/emacs/manual/html_node/eintr/][elisp]]).
It comes with a broad array of useful functions for text-editing, and Doom adds
a few handy little convenience functions.
Want to add a keybinding to delete the previous line? It's as easy as
#+NAME: Keybinding to delete the previous line
#+BEGIN_SRC emacs-lisp :tangle no
(map! "C-d"
(cmd! (previous-line)
(kill-line)
(forward-line)))
#+END_SRC
How about another example, say you want to be presented with a list of currently
open /buffers/ (think files, almost) when you split the window. It's as simple as
#+NAME: Prompt for buffer after split
#+BEGIN_SRC emacs-lisp :tangle no
(defadvice! prompt-for-buffer (&rest _)
:after 'window-split (switch-to-buffer))
#+END_SRC
Want to test it out? You don't need to save and restart, you can just /evaluate
the expression/ within your current Emacs instance and try it immediately! This
editor is, after all, a Lisp interpreter.
Want to tweak the behaviour? Just re-evaluate your new version --- it's a
super-tight iteration loop.
** Editor comparison
[[xkcd:378]]
Over the years I have tried out (spent at least a year using as my primary
editor) the following applications
- Python IDLE
- Komodo Edit
- Brackets
- ;VSCode
- and now, Emacs
I have attempted to quantify aspects of my impressions of them below.
#+PLOT: transpose:yes type:radar min:0 max:4 ticks:4 file:"misc/editor-comparison.png"
| Editor | Extensibility | Ecosystem | Ease of Use | Comfort | Completion | Performance |
|-------------+---------------+-----------+-------------+---------+------------+-------------|
| IDLE | 1 | 1 | 3 | 1 | 1 | 2 |
| ;VSCode | 3 | 3 | 4 | 3.5 | 4 | 3 |
| Brackets | 2.5 | 2 | 3 | 3 | 2.5 | 2 |
| Emacs | 4 | 4 | 2 | 4 | 3.5 | 3 |
| Komodo Edit | 2 | 1 | 3 | 2 | 2 | 2 |
#+attr_html: :class invertible :alt Radar chart comparing my thoughts on a few editors.
[[https://tecosaur.com/lfs/emacs-config/editor-comparison.png]]
** Notes for the unwary adventurer
2020-05-22 15:39:22 +00:00
If you like the look of this, that's marvellous, and I'm really happy that I've
made something which you may find interesting, however:
#+begin_warning
This config is /insidious/. Copying the whole thing blindly can easily lead to
undesired effects. I recommend copying chunks instead.
#+end_warning
If you are so bold as to wish to steal bits of my config (or if I upgrade and
wonder why things aren't working), here's a list of sections which rely on
2020-07-17 18:25:27 +00:00
external setup (i.e. outside of this config).
2020-04-29 08:47:48 +00:00
+ libvterm :: I rely on my distro-provided =libvterm.so=, [[*VTerm][vterm]] sets a compile flag.
If this causes issues, just delete the ~(setq term-module-cmake-args ...)~ bit.
2020-04-29 08:47:48 +00:00
+ dictionary :: I've downloaded a custom [[http://app.aspell.com/create][SCOWL]] dictionary, which I use in [[*Ispell][ispell]].
If this causes issues, just delete the ~(setq ispell-dictionary ...)~ bit.
+ uni-units file :: I've got a file in =~/.org/.uni-units= which I use in ~org-capture~
If this causes issues, just remove the reference to that file in [[*Capture][Capture]] and
instances of ~unit-prompt~ used in ~(doct ...)~
Oh, did I mention that I started this config when I didn't know any =elisp=, and
this whole thing is a hack job? If you can suggest any improvements, please do
2020-07-17 18:25:27 +00:00
so, no matter how much criticism you include I'll appreciate it :)
[[xkcd:1513]]
2020-04-29 10:32:57 +00:00
*** Extra Requirements
The lovely ~doom doctor~ is good at diagnosing most missing things, but here are a
few extras.
+ A [[https://www.tug.org/texlive/][LaTeX Compiler]] is required for the mathematics rendering performed in [[#org][Org]],
and by [[*CalcTeX][CalcTeX]].
+ I use the [[https://overpassfont.org/][Overpass]] font as a go-to sans serif.
It's used as my ~doom-variable-pitch-font~ and in the graph generated by [[*Roam][Roam]].
I have chosen it because it possesses a few characteristics I consider
desirable, namely:
- A clean, and legible style. Highway-style fonts tend to be designed to be
clear at a glance, and work well with a thicker weight, and this is inspired
by /Highway Gothic/.
- It's slightly quirky. Look at the diagonal cut on stems for example.
2020-07-17 18:25:27 +00:00
Helvetica is a masterful design, but I like a bit more pizzazz now and then.
2020-04-29 10:32:57 +00:00
+ A few LSP servers. Take a look at [[file:init.el][init.el]] to see which modules have the ~+lsp~ flag.
+ The =cargo-script= rust crate is required for evaluation of rust blocks by babel.
As described in the README for [[https://github.com/micanzhang/ob-rust][ob-rust]]. Like ~delta~, this can just be installed
using cargo.
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "cargo-script") "no" "setup.sh")
2020-04-29 10:32:57 +00:00
cargo install cargo-script
#+END_SRC
+ The [[https://github.com/dandavison/delta/][Delta]] binary. It's packaged for some distributions but I installed it with
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "delta") "no" "setup.sh")
cargo install git-delta
#+END_SRC
2020-09-02 01:53:25 +00:00
+ The =theme-magic= package requires the ~wal~ (=pywal=) executable. If this is
packaged for you, great! If not, it's just a quick ~pip install~ away.
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "wal") "no" "setup.sh")
sudo python3 -m pip install pywal
#+END_SRC
2020-05-09 19:00:48 +00:00
** Current Issues
*** Magit push in daemon
2020-07-17 18:25:27 +00:00
Quite often trying to push to a remote in the Emacs daemon produces as error like this:
2020-05-09 19:00:48 +00:00
#+BEGIN_SRC fundamental
128 git … push -v origin refs/heads/master\:refs/heads/master
Pushing to git@github.com:tecosaur/emacs-config.git
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
#+END_SRC
*** CalcTeX brings up compilation buffer
With my [[*Calc][Calc]] hook, the first call of =M-x calc= brings up a compilation buffer
from CalcTeX. I'm guessing this is from the compilation of the preamble / ~.fmt~ file.
2020-05-24 12:46:30 +00:00
*** Unread emails doesn't work across Emacs instances
It would be nice if it did, so that I could have the Emacs-daemon hold the
active mu4e session, but still get that information. In this case I'd want to
change the action to open the Emacs daemon, but it should be possible.
This would probably involve hooking into the daemon's modeline update function
to write to a temporary file, and having a file watcher started in other Emacs
instances, in a similar manner to [[*Rebuild mail index while using mu4e][Rebuild mail index while using mu4e]].
2020-05-15 10:07:10 +00:00
* Rudimentary configuration
2020-02-03 05:28:15 +00:00
Make this file run (slightly) faster with lexical binding (see [[https://nullprogram.com/blog/2016/12/22/][this blog post]]
for more info).
2020-04-29 10:33:15 +00:00
#+BEGIN_SRC emacs-lisp :comments no
2020-02-03 05:28:15 +00:00
;;; config.el -*- lexical-binding: t; -*-
#+END_SRC
** Personal Information
It's useful to have some basic personal information
#+BEGIN_SRC emacs-lisp
2020-05-20 13:49:16 +00:00
(setq user-full-name "TEC"
user-mail-address "tec@tecosaur.com")
#+END_SRC
Apparently this is used by ~GPG~, and all sorts of other things.
2020-05-20 13:49:16 +00:00
Speaking of ~GPG~, I want to use =~/.authsource.gpg= instead of the default in
=~/.emacs.d=. Why? Because my home directory is already cluttered, so this won't
2020-07-19 16:33:28 +00:00
make a difference, and I don't want to accidentaly purge this file (I have done
2020-05-20 13:49:16 +00:00
~rm -rf~/.emac.d~ before). I also want to cache as much as possible, as my home
machine is pretty safe, and my laptop is shutdown a lot.
#+BEGIN_SRC emacs-lisp
(setq auth-sources '("~/.authinfo.gpg")
auth-source-cache-expiry nil) ; default is 7200 (2h)
#+END_SRC
** Better defaults
*** Simple settings
Browsing the web and seeing [[https://github.com/angrybacon/dotemacs/blob/master/dotemacs.org#use-better-defaults][angrybacon/dotemacs]] and comparing with the values
shown by =SPC h v= and selecting what I thought looks good, I've ended up adding the following:
#+BEGIN_SRC emacs-lisp
(setq-default
delete-by-moving-to-trash t ; Delete files to trash
window-combination-resize t ; take new window space from all other windows (not just current)
x-stretch-cursor t) ; Stretch cursor to the glyph width
2020-02-02 18:21:19 +00:00
(setq undo-limit 80000000 ; Raise undo-limit to 80Mb
2020-03-04 15:27:08 +00:00
evil-want-fine-undo t ; By default while in insert all changes are one big blob. Be more granular
auto-save-default t ; Nobody likes to loose work, I certainly don't
truncate-string-ellipsis "…") ; Unicode ellispis are nicer than "...", and also save /precious/ space
(display-time-mode 1) ; Enable time in the mode-line
2020-07-19 05:23:10 +00:00
(unless (equal "Battery status not available"
(battery))
(display-battery-mode 1)) ; On laptops it's nice to know how much power you have
(global-subword-mode 1) ; Iterate through CamelCase words
#+END_SRC
*** Fullscreen
2020-07-19 16:33:28 +00:00
I also like the idea of fullscreen-ing when opened by ~Emacs~ or the ~.desktop~ file.
#+BEGIN_SRC emacs-lisp
(if (eq initial-window-system 'x) ; if started by emacs command or desktop file
(toggle-frame-maximized)
(toggle-frame-fullscreen))
#+END_SRC
*** Auto-customisations
By default changes made via a customisation interface are added to =init.el=.
I prefer the idea of using a separate file for this. We just need to change a
setting, and load it if it exists.
#+BEGIN_SRC emacs-lisp
2020-02-26 12:50:01 +00:00
(setq-default custom-file (expand-file-name ".custom.el" doom-private-dir))
(when (file-exists-p custom-file)
(load custom-file))
#+END_SRC
*** Windows
I find it rather handy to be asked which buffer I want to see after splitting
the window. Let's make that happen.
First, we'll enter the new window
#+BEGIN_SRC emacs-lisp
(setq evil-vsplit-window-right t
evil-split-window-below t)
#+END_SRC
Then, we'll pull up ~ivy~
#+BEGIN_SRC emacs-lisp
(defadvice! prompt-for-buffer (&rest _)
:after '(evil-window-split evil-window-vsplit)
2020-02-09 15:16:25 +00:00
(+ivy/switch-buffer))
#+END_SRC
2020-04-14 09:12:09 +00:00
Oh, and previews are nice
#+BEGIN_SRC emacs-lisp
(setq +ivy-buffer-preview t)
#+END_SRC
Window rotation is nice, and can be found under =SPC w r= and =SPC w R=.
/Layout/ rotation is also nice though. Let's stash this under =SPC w SPC=, inspired
by Tmux's use of =C-b SPC= to rotate windows.
We could also do with adding the missing arrow-key variants of the window
navigation/swapping commands.
#+BEGIN_SRC emacs-lisp
(map! :map evil-window-map
"SPC" #'rotate-layout
;; Navigation
"<left>" #'evil-window-left
"<down>" #'evil-window-down
"<up>" #'evil-window-up
"<right>" #'evil-window-right
;; Swapping windows
"C-<left>" #'+evil/window-move-left
"C-<down>" #'+evil/window-move-down
"C-<up>" #'+evil/window-move-up
"C-<right>" #'+evil/window-move-right)
#+END_SRC
2020-01-29 12:46:48 +00:00
*** Buffer defaults
I'd much rather have my new buffers in ~org-mode~ than ~fundamental-mode~, hence
#+BEGIN_SRC emacs-lisp
;; (setq-default major-mode 'org-mode)
2020-01-29 12:46:48 +00:00
#+END_SRC
For some reason this + the mixed pitch hook causes issues with hydra and so I'll
just need to resort to =SPC b o= for now.
** Doom configuration
*** Visual Settings
**** Font Face
'Fira Code' is nice, and 'Overpass' makes for a nice sans companion. We just need to
2020-06-29 18:38:21 +00:00
fiddle with the font sizes a tad so that they visually match. Just for fun I'm
trying out JetBrains Mono though. So far I have mixed feelings on it, some
aspects are nice, but on others I prefer Fira.
#+BEGIN_SRC emacs-lisp
2020-05-16 07:43:40 +00:00
(setq doom-font (font-spec :family "JetBrains Mono" :size 24)
doom-big-font (font-spec :family "JetBrains Mono" :size 36)
2020-05-16 09:34:29 +00:00
doom-variable-pitch-font (font-spec :family "Overpass" :size 24)
doom-serif-font (font-spec :family "IBM Plex Mono" :weight 'light))
#+END_SRC
2020-08-12 10:21:13 +00:00
#+attr_html: :class invertible :alt Screenshot of the fonts within Emacs.
[[https://tecosaur.com/lfs/emacs-config/screenshots/font-face.png]]
2020-06-02 05:06:47 +00:00
**** Theme and modeline
2020-06-02 13:51:19 +00:00
~doom-one~ is nice and all, but I find the ~vibrant~ variant nicer. Oh, and with the
nice selection doom provides there's no reason for me to want the defaults.
#+BEGIN_SRC emacs-lisp
(setq doom-theme 'doom-vibrant)
2020-06-02 13:51:19 +00:00
(delq! t custom-theme-load-path)
#+END_SRC
However, by default ~red~ text is used in the ~modeline~, so let's make that orange
so I don't feel like something's gone /wrong/ when editing files.
#+BEGIN_SRC emacs-lisp
(custom-set-faces!
'(doom-modeline-buffer-modified :foreground "orange"))
#+END_SRC
While we're modifying the modeline, =LF UTF-8= is the default file encoding, and
thus not worth noting in the modeline. So, let's conditionally hide it.
#+BEGIN_SRC emacs-lisp
(defun doom-modeline-conditional-buffer-encoding ()
"We expect the encoding to be LF UTF-8, so only show the modeline when this is not the case"
(setq-local doom-modeline-buffer-encoding
(unless (or (eq buffer-file-coding-system 'utf-8-unix)
(eq buffer-file-coding-system 'utf-8)))))
(add-hook 'after-change-major-mode-hook #'doom-modeline-conditional-buffer-encoding)
#+END_SRC
**** Miscellaneous
Relative line numbers are fantastic for knowing how far away line numbers are,
then =ESC 12 <UP>= gets you exactly where you think.
#+BEGIN_SRC emacs-lisp
(setq display-line-numbers-type 'relative)
#+END_SRC
2020-02-06 17:11:35 +00:00
I'd like some slightly nicer default buffer names
#+BEGIN_SRC emacs-lisp
(setq doom-fallback-buffer-name "► Doom"
+doom-dashboard-name "► Doom")
#+END_SRC
There's a bug with the modeline in insert mode for org documents ([[https://github.com/seagle0128/doom-modeline/issues/300][issue]]), so
2020-02-09 00:12:19 +00:00
#+BEGIN_SRC emacs-lisp
(custom-set-faces! '(doom-modeline-evil-insert-state :weight bold :foreground "#339CDB"))
2020-02-09 00:12:19 +00:00
#+END_SRC
*** Some helper macros
There are a few handy macros added by doom, namely
- ~load!~ for loading external ~.el~ files relative to this one
- ~use-package!~ for configuring packages
2020-07-19 16:33:28 +00:00
- ~add-load-path!~ for adding directories to the ~load-path~ where ~Emacs~ looks when
you load packages with ~require~ or ~use-package~
- ~map!~ for binding new keys
** Other things
*** Editor interaction
**** Mouse buttons
#+BEGIN_SRC emacs-lisp
(map! :n [mouse-8] #'better-jumper-jump-backward
:n [mouse-9] #'better-jumper-jump-forward)
#+END_SRC
2020-02-06 15:31:44 +00:00
*** Window title
I'd like to have just the buffer name, then if applicable the project folder
#+BEGIN_SRC emacs-lisp
(setq frame-title-format
'(""
2020-04-28 18:41:46 +00:00
(:eval
(if (s-contains-p org-roam-directory (or buffer-file-name ""))
(replace-regexp-in-string ".*/[0-9]*-?" "🢔 " buffer-file-name)
2020-04-28 18:41:46 +00:00
"%b"))
2020-02-06 15:31:44 +00:00
(:eval
(let ((project-name (projectile-project-name)))
(unless (string= "-" project-name)
2020-02-16 05:23:19 +00:00
(format (if (buffer-modified-p) " ◉ %s" "  ●  %s") project-name))))))
2020-02-06 15:31:44 +00:00
#+END_SRC
2020-03-27 04:20:42 +00:00
*** Splash screen
Emacs can render an image as the splash screen, and [[https://github.com/MarioRicalde][@MarioRicalde]] came up with a
2020-07-19 16:33:28 +00:00
cracker! He's also provided me with a nice Emacs-style /E/, which is good for
smaller windows. *@MarioRicalde* you have my sincere thanks, you're great!
[[file:misc/splash-images/blackhole-lines.svg]]
By incrementally stripping away the outer layers of the logo one can obtain
quite a nice resizing effect.
2020-03-27 04:20:42 +00:00
#+BEGIN_SRC emacs-lisp
(defvar fancy-splash-image-template
(expand-file-name "misc/splash-images/blackhole-lines-template.svg" doom-private-dir)
"Default template svg used for the splash image, with substitutions from ")
(defvar fancy-splash-image-nil
(expand-file-name "misc/splash-images/transparent-pixel.png" doom-private-dir)
"An image to use at minimum size, usually a transparent pixel")
(setq fancy-splash-sizes
`((:height 500 :min-height 50 :padding (0 . 4) :template ,(expand-file-name "misc/splash-images/blackhole-lines-0.svg" doom-private-dir))
(:height 440 :min-height 42 :padding (1 . 4) :template ,(expand-file-name "misc/splash-images/blackhole-lines-0.svg" doom-private-dir))
(:height 400 :min-height 38 :padding (1 . 4) :template ,(expand-file-name "misc/splash-images/blackhole-lines-1.svg" doom-private-dir))
(:height 350 :min-height 36 :padding (1 . 3) :template ,(expand-file-name "misc/splash-images/blackhole-lines-2.svg" doom-private-dir))
(:height 300 :min-height 34 :padding (1 . 3) :template ,(expand-file-name "misc/splash-images/blackhole-lines-3.svg" doom-private-dir))
(:height 250 :min-height 32 :padding (1 . 2) :template ,(expand-file-name "misc/splash-images/blackhole-lines-4.svg" doom-private-dir))
(:height 200 :min-height 30 :padding (1 . 2) :template ,(expand-file-name "misc/splash-images/blackhole-lines-5.svg" doom-private-dir))
(:height 100 :min-height 24 :padding (1 . 2) :template ,(expand-file-name "misc/splash-images/emacs-e-template.svg" doom-private-dir))
(:height 0 :min-height 0 :padding (0 . 0) :file ,fancy-splash-image-nil)))
(defvar fancy-splash-sizes
`((:height 500 :min-height 50 :padding (0 . 2))
(:height 440 :min-height 42 :padding (1 . 4))
(:height 330 :min-height 35 :padding (1 . 3))
(:height 200 :min-height 30 :padding (1 . 2))
(:height 0 :min-height 0 :padding (0 . 0) :file ,fancy-splash-image-nil))
"list of plists with the following properties
:height the height of the image
:min-height minimum `frame-height' for image
:padding `+doom-dashboard-banner-padding' to apply
:template non-default template file
:file file to use instead of template")
(defvar fancy-splash-template-colours
'(("$colour1" . keywords) ("$colour2" . type) ("$colour3" . base5) ("$colour4" . base8))
"list of colour-replacement alists of the form (\"$placeholder\" . 'theme-colour) which applied the template")
(unless (file-exists-p (expand-file-name "theme-splashes" doom-cache-dir))
(make-directory (expand-file-name "theme-splashes" doom-cache-dir) t))
(defun fancy-splash-filename (theme-name height)
(expand-file-name (concat (file-name-as-directory "theme-splashes")
(symbol-name doom-theme)
"-" (number-to-string height) ".svg")
doom-cache-dir))
(defun fancy-splash-clear-cache ()
"Delete all cached fancy splash images"
(interactive)
(delete-directory (expand-file-name "theme-splashes" doom-cache-dir) t)
(message "Cache cleared!"))
(defun fancy-splash-generate-image (template height)
2020-09-27 08:00:02 +00:00
"Read TEMPLATE and create an image if HEIGHT with colour substitutions as
described by `fancy-splash-template-colours' for the current theme"
(with-temp-buffer
(insert-file-contents template)
(re-search-forward "$height" nil t)
(replace-match (number-to-string height) nil nil)
(dolist (substitution fancy-splash-template-colours)
2020-09-27 08:00:02 +00:00
(goto-char (point-min))
(while (re-search-forward (car substitution) nil t)
(replace-match (doom-color (cdr substitution)) nil nil)))
2020-03-28 04:18:03 +00:00
(write-region nil nil
(fancy-splash-filename (symbol-name doom-theme) height) nil nil)))
(defun fancy-splash-generate-images ()
"Perform `fancy-splash-generate-image' in bulk"
(dolist (size fancy-splash-sizes)
(unless (plist-get size :file)
(fancy-splash-generate-image (or (plist-get size :file)
(plist-get size :template)
fancy-splash-image-template)
(plist-get size :height)))))
(defun ensure-theme-splash-images-exist (&optional height)
(unless (file-exists-p (fancy-splash-filename
(symbol-name doom-theme)
(or height
(plist-get (car fancy-splash-sizes) :height))))
(fancy-splash-generate-images)))
(defun get-appropriate-splash ()
(let ((height (frame-height)))
(cl-some (lambda (size) (when (>= height (plist-get size :min-height)) size))
fancy-splash-sizes)))
(setq fancy-splash-last-size nil)
(setq fancy-splash-last-theme nil)
2020-09-27 08:00:02 +00:00
(defun set-appropriate-splash (&rest _)
(let ((appropriate-image (get-appropriate-splash)))
(unless (and (equal appropriate-image fancy-splash-last-size)
(equal doom-theme fancy-splash-last-theme)))
(unless (plist-get appropriate-image :file)
(ensure-theme-splash-images-exist (plist-get appropriate-image :height)))
(setq fancy-splash-image
(or (plist-get appropriate-image :file)
(fancy-splash-filename (symbol-name doom-theme) (plist-get appropriate-image :height))))
(setq +doom-dashboard-banner-padding (plist-get appropriate-image :padding))
(setq fancy-splash-last-size appropriate-image)
(setq fancy-splash-last-theme doom-theme)
(+doom-dashboard-reload)))
2020-03-27 04:20:42 +00:00
(add-hook 'window-size-change-functions #'set-appropriate-splash)
(add-hook 'doom-load-theme-hook #'set-appropriate-splash)
2020-03-27 04:20:42 +00:00
#+END_SRC
2020-08-12 10:21:13 +00:00
#+attr_html: :class invertible :alt The splash screen, just loaded.
[[https://tecosaur.com/lfs/emacs-config/screenshots/splash-screen.png]]
2020-02-16 05:12:36 +00:00
*** Systemd daemon
For running a systemd service for a Emacs server I have the following
#+BEGIN_SRC systemd :tangle ~/.config/systemd/user/emacs.service :mkdirp yes
2020-02-16 05:12:36 +00:00
[Unit]
Description=Emacs server daemon
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
[Service]
Type=forking
ExecStart=/usr/bin/emacs --daemon
ExecStop=/usr/bin/emacsclient --eval "(progn (setq kill-emacs-hook nil) (kill emacs))"
Environment=SSH_AUTH_SOCK=%t/keyring/ssh
Restart=on-failure
[Install]
WantedBy=default.target
#+END_SRC
which is then enabled by
#+BEGIN_SRC shell :tangle (if (string= "enabled\n" (shell-command-to-string "systemctl --user is-enabled emacs.service")) "no" "setup.sh")
2020-02-16 05:12:36 +00:00
systemctl --user enable emacs.service
#+END_SRC
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.
2020-06-29 18:45:17 +00:00
#+BEGIN_SRC conf :tangle ~/.local/share/applications/emacs-client.desktop :mkdirp yes
[Desktop Entry]
Name=Emacs client
GenericName=Text Editor
Comment=A flexible platform for end-user applications
MimeType=text/english;text/plain;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 --alternate-editor="" %F
Icon=emacs
Type=Application
Terminal=false
Categories=TextEditor;Utility;
StartupWMClass=Emacs
Keywords=Text;Editor;
X-KDE-StartupNotify=false
#+END_SRC
When the daemon is running, I almost always want to do a few particular things
2020-08-08 08:40:19 +00:00
with it, so I may as well eat the load time at startup. We also want to keep
=mu4e= running.
It would be good to start the IRC client (=circe=) too, but that seems to have
issues when started in a non-graphical session.
#+BEGIN_SRC emacs-lisp
(defun greedily-do-daemon-setup ()
(when (daemonp)
(require 'org)
(require 'mu4e)
(setq mu4e-confirm-quit t)
(setq mu4e-lock-greedy t)
(setq mu4e-lock-relaxed t)
(mu4e-lock-add-watcher)
(when (mu4e-lock-avalible t)
(mu4e~start))))
(add-hook 'emacs-startup-hook #'greedily-do-daemon-setup)
#+END_SRC
* Package loading
:PROPERTIES:
2020-04-10 04:31:36 +00:00
:header-args:emacs-lisp: :tangle "packages.el" :comments link
:END:
This file shouldn't be byte compiled.
2020-04-29 13:20:54 +00:00
#+BEGIN_SRC emacs-lisp :tangle "packages.el" :comments no
;; -*- no-byte-compile: t; -*-
#+END_SRC
** Loading instructions
:PROPERTIES:
2020-03-23 02:59:27 +00:00
:header-args:emacs-lisp: :tangle no
:END:
This is where you install packages, by declaring them with the ~package!~
macro, then running ~doom refresh~ on the command line. You'll need to
restart Emacs for your changes to take effect! Or at least, run =M-x doom/reload=.
WARNING: Don't disable core packages listed in ~~/.emacs.d/core/packages.el~.
Doom requires these, and disabling them may have terrible side effects.
*** Packages in MELPA/ELPA/emacsmirror
To install ~some-package~ from MELPA, ELPA or emacsmirror:
#+BEGIN_SRC emacs-lisp
(package! some-package)
#+END_SRC
*** Packages from git repositories
To install a package directly from a particular repo, you'll need to specify
a ~:recipe~. You'll find documentation on what ~:recipe~ accepts [[https://github.com/raxod502/straight.el#the-recipe-format][here]]:
#+BEGIN_SRC emacs-lisp
(package! another-package
:recipe (:host github :repo "username/repo"))
#+END_SRC
If the package you are trying to install does not contain a ~PACKAGENAME.el~
file, or is located in a subdirectory of the repo, you'll need to specify
~:files~ in the ~:recipe~:
#+BEGIN_SRC emacs-lisp
(package! this-package
:recipe (:host github :repo "username/repo"
:files ("some-file.el" "src/lisp/*.el")))
#+END_SRC
2020-01-09 18:00:50 +00:00
*** Disabling built-in packages
If you'd like to disable a package included with Doom, for whatever reason,
you can do so here with the ~:disable~ property:
#+BEGIN_SRC emacs-lisp
(package! builtin-package :disable t)
#+END_SRC
You can override the recipe of a built in package without having to specify
all the properties for ~:recipe~. These will inherit the rest of its recipe
from Doom or MELPA/ELPA/Emacsmirror:
#+BEGIN_SRC emacs-lisp
(package! builtin-package :recipe (:nonrecursive t))
(package! builtin-package-2 :recipe (:repo "myfork/package"))
#+END_SRC
Specify a ~:branch~ to install a package from a particular branch or tag.
This is required for some packages whose default branch isn't 'master' (which
our package manager can't deal with; see [[https://github.com/raxod502/straight.el/issues/279][raxod502/straight.el#279]])
#+BEGIN_SRC emacs-lisp
(package! builtin-package :recipe (:branch "develop"))
#+END_SRC
2020-02-16 06:13:35 +00:00
** General packages
2020-01-20 18:08:53 +00:00
*** Window management
#+BEGIN_SRC emacs-lisp
(package! rotate :pin "091b5ac4fc310773253efb317e3dbe8e46959ba6")
2020-01-20 18:08:53 +00:00
#+END_SRC
*** Fun
Sometimes one just wants a little fun.
XKCD comics are fun.
#+BEGIN_SRC emacs-lisp
(package! xkcd :pin "66e928706fd660cfdab204c98a347b49c4267bdf")
2020-01-20 18:08:53 +00:00
#+END_SRC
Every so often, you want everyone else to /know/ that you're typing, or just to
amuse oneself. Introducing: typewriter sounds!
#+BEGIN_SRC emacs-lisp
(package! selectric-mode :pin "bb9e66678f34e9bc23624ff6292cf5e7857e8e5f")
2020-01-20 18:08:53 +00:00
#+END_SRC
Hey, let's get the weather in here while we're at it.
2020-05-17 03:06:32 +00:00
Unfortunately this seems slightly unmaintained ([[https://github.com/bcbcarl/emacs-wttrin/pulls][few open bugfix PRs]]) so let's
2020-06-02 09:56:56 +00:00
roll our [[file:lisp/wttrin.el][own version]].
2020-01-20 18:08:53 +00:00
#+BEGIN_SRC emacs-lisp
2020-05-17 03:06:32 +00:00
(package! wttrin :recipe (:local-repo "lisp" :no-byte-compile t))
2020-01-20 18:08:53 +00:00
#+END_SRC
Why not flash words on the screen. Why not --- hey, it could be fun.
#+BEGIN_SRC emacs-lisp
(package! spray :pin "00638bc916227f2f961013543d10e85a43a32e29")
2020-01-20 18:08:53 +00:00
#+END_SRC
With all our fancy Emacs themes, my terminal is missing out!
2020-01-20 18:08:53 +00:00
#+BEGIN_SRC emacs-lisp
(package! theme-magic :pin "844c4311bd26ebafd4b6a1d72ddcc65d87f074e3")
2020-01-20 18:08:53 +00:00
#+END_SRC
2020-04-09 17:15:17 +00:00
What's even the point of using Emacs unless you're constantly telling everyone
about it?
#+BEGIN_SRC emacs-lisp
2020-06-29 19:17:52 +00:00
(package! elcord :pin "01b26d1af2f33a7c7c5a1c24d8bfb6d40115a7b0")
2020-04-09 17:15:17 +00:00
#+END_SRC
2020-06-02 09:56:56 +00:00
For some reason, I find myself demoing Emacs every now and then. Showing what
keyboard stuff I'm doing on-screen seems helpful. While [[https://gitlab.com/screenkey/screenkey][screenkey]] does exist,
having something that doesn't cover up screen content is nice.
2020-08-12 10:21:13 +00:00
#+attr_html: :class invertible :alt Screenshot of Keycast-mode in action
[[https://tecosaur.com/lfs/emacs-config/screenshots/keycast.png]]
2020-06-02 09:56:56 +00:00
#+BEGIN_SRC emacs-lisp
2020-06-29 19:17:52 +00:00
(package! keycast :pin "038475c178e90c7bad64d113db26d42cad60e149")
2020-06-02 09:56:56 +00:00
#+END_SRC
let's just make sure this is lazy-loaded appropriately.
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package! keycast
:commands keycast-mode
:config
(define-minor-mode keycast-mode
"Show current command and its key binding in the mode line."
:global t
(if keycast-mode
(progn
(add-hook 'pre-command-hook 'keycast-mode-line-update t)
(add-to-list 'global-mode-string '("" mode-line-keycast " ")))
(remove-hook 'pre-command-hook 'keycast-mode-line-update)
(setq global-mode-string (remove '("" mode-line-keycast " ") global-mode-string))))
(custom-set-faces!
'(keycast-command :inherit doom-modeline-debug
:height 0.9)
'(keycast-key :inherit custom-modified
:height 1.1
:weight bold)))
#+END_SRC
2020-07-29 15:48:00 +00:00
In a similar manner, [[https://gitlab.com/ambrevar/emacs-gif-screencast][gif-screencast]] may come in handy.
#+BEGIN_SRC emacs-lisp
(package! gif-screencast)
#+END_SRC
We can lazy load this using the start/stop commands.
I initially installed ~scrot~ for this, since it was the default capture program.
However it raised ~glib error: Saving to file ... failed~ each time it was run.
Google didn't reveal any easy fixed, so I switched to [[https://github.com/naelstrof/maim][maim]]. We now need to pass
it the window ID. This doesn't change throughout the lifetime of an emacs
instance, so as long as a single window is used ~xdotool getactivewindow~ will
give a satisfactory result.
It seems that when new colours appear, that tends to make ~gifsicle~ introduce
artefacts. To avoid this we pre-populate the colour map using the current doom
theme.
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package! gif-screencast
:commands gif-screencast-mode
:config
(map! :map gif-screencast-mode-map
:g "<f8>" #'gif-screencast-toggle-pause
:g "<f9>" #'gif-screencast-stop)
2020-07-29 15:48:00 +00:00
(setq gif-screencast-program "maim"
gif-screencast-args `("--quality" "3" "-i" ,(string-trim-right
(shell-command-to-string
"xdotool getactivewindow")))
gif-screencast-optimize-args '("--batch" "--optimize=3" "--usecolormap=/tmp/doom-color-theme"))
(defun gif-screencast-write-colormap ()
(f-write-text
(replace-regexp-in-string
"\n+" "\n"
(mapconcat (lambda (c) (if (listp (cdr c))
(cadr c))) doom-themes--colors "\n"))
'utf-8
"/tmp/doom-color-theme" ))
(gif-screencast-write-colormap)
(add-hook 'doom-load-theme-hook #'gif-screencast-write-colormap))
#+END_SRC
2020-04-29 10:33:15 +00:00
*** Improving features
2020-01-20 18:08:53 +00:00
**** Flyspell-lazy
2020-01-09 18:01:52 +00:00
To alleviate some [[Flyspell][issues with flyspell]]
#+BEGIN_SRC emacs-lisp
(package! flyspell-lazy :pin "3ebf68cc9eb10c972a2de8d7861cbabbbce69570")
2020-01-09 18:01:52 +00:00
#+END_SRC
2020-02-03 04:57:08 +00:00
**** CalcTeX
This is a nice extension to ~calc~
#+BEGIN_SRC emacs-lisp
(package! calctex :recipe (:host github :repo "johnbcoughlin/calctex"
2020-07-25 05:07:46 +00:00
:files ("*.el")) :pin "7fa2673c64e259e04aef684ccf09ef85570c388b")
2020-02-03 04:57:08 +00:00
#+END_SRC
2020-02-16 06:13:35 +00:00
**** ESS
View data frames better with
2020-02-16 06:13:35 +00:00
#+BEGIN_SRC emacs-lisp
(package! ess-view :pin "d4e5a340b7bcc58c434867b97923094bd0680283")
2020-02-16 06:13:35 +00:00
#+END_SRC
2020-04-29 10:33:15 +00:00
**** Magit Delta
[[https://github.com/dandavison/delta/][Delta]] is a git diff syntax highlighter written in rust. The author also wrote a
package to hook this into the magit diff view. This requires the ~delta~ binary.
#+BEGIN_SRC emacs-lisp
2020-05-15 03:13:18 +00:00
;; (package! magit-delta :recipe (:host github :repo "dandavison/magit-delta") :pin "0c7d8b2359")
2020-04-29 10:33:15 +00:00
#+END_SRC
2020-05-16 09:34:29 +00:00
**** Info colours
This makes manual pages nicer to look at :)
Variable pitch fontification + colouring
#+attr_html: :class invertible :style width:80% :alt Example info-colours page.
[[https://tecosaur.com/lfs/emacs-config/screenshots/info-colours.png]]
2020-05-16 09:34:29 +00:00
#+BEGIN_SRC emacs-lisp
(package! info-colors :pin "47ee73cc19b1049eef32c9f3e264ea7ef2aaf8a5")
2020-05-16 09:34:29 +00:00
#+END_SRC
2020-07-25 05:35:52 +00:00
**** Large files
The /very large files/ mode loads large files in chunks, allowing one to open
ridiculously large files.
#+BEGIN_SRC emacs-lisp
(package! vlf :recipe (:host github :repo "m00natic/vlfi" :files ("*.el"))
:pin "cc02f2533782d6b9b628cec7e2dcf25b2d05a27c")
#+END_SRC
To make VLF available without delaying startup, we'll just load it in quiet moments.
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package! vlf-setup
:defer-incrementally vlf-tune vlf-base vlf-write vlf-search vlf-occur vlf-follow vlf-ediff vlf)
#+END_SRC
**** Definitions
Doom already loads =define-word=, and provides it's own definition service using
[[https://github.com/gromnitsky/wordnut][wordnut]]. However, using an offline dictionary possess a few compelling
advantages, namely:
+ speed
+ integration of multiple dictionaries
[[http://goldendict.org/][GoldenDict]] seems like the best option currently avalible, but lacks a CLI.
Hence, we'll fall back to [[https://dushistov.github.io/sdcv/][sdcv]] (a CLI version of StarDict) for now.
To interface with this, we'll use a my =lexic= package.
#+attr_html: :class invertible :alt Screenshot of the lexic-mode view of "literate"
[[https://tecosaur.com/lfs/emacs-config/screenshots/lexic.png]]
#+BEGIN_SRC emacs-lisp
(package! lexic :recipe (:local-repo "lisp/lexic"))
#+END_SRC
Given that a request for a CLI is the most upvoted issue on GitHub for
GoldenDict, it's likely we'll be able to switch from ~sdcv~ to that in the future.
Since GoldenDict supports StarDict files, I expect this will be a relatively
painless switch.
2020-05-23 04:17:08 +00:00
*** Email
2020-05-24 03:38:36 +00:00
It's nice to send out fancy HTML emails. The =org-msg= package allows to write
emails in org mode, and send as an HTML multipart email. We can setup some CSS
to be inlined, render LaTeX fragments, and all those goodies!
2020-05-23 04:17:08 +00:00
#+BEGIN_SRC emacs-lisp
(package! org-msg)
#+END_SRC
2020-05-24 03:38:36 +00:00
To actually get notifications, we can use =mu4e-alert=.
#+BEGIN_SRC emacs-lisp
(package! mu4e-alert)
#+END_SRC
2020-02-16 06:13:35 +00:00
** Language packages
2020-05-18 16:01:23 +00:00
*** LaTeX
2020-06-29 18:47:34 +00:00
For mathematical convenience, WIP
#+BEGIN_SRC emacs-lisp
(package! auto-latex-snippets
:recipe (:local-repo "lisp/Auto-LaTeX-Snippets"))
2020-06-29 18:47:34 +00:00
#+END_SRC
And some basic config
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package! auto-latex-snippets
:hook (LaTeX-mode . auto-latex-snippets-mode)
:config
(defun als-tex-fold-maybe ()
(unless (equal "/" als-transient-snippet-key)
(+latex-fold-last-macro-a)))
(add-hook 'als-post-snippet-expand-hook #'als-tex-fold-maybe))
#+END_SRC
2020-02-16 06:13:35 +00:00
*** Org Mode
**** Improve agenda/capture
The agenda is nice, but a souped up version is nicer.
#+BEGIN_SRC emacs-lisp
(package! org-super-agenda :pin "dd0d104c269fab9ebe5af7009bc1dd2a3a8f3c12")
#+END_SRC
Similarly ~doct~ (Declarative Org Capture Templates) seems to be a nicer way to
set up org-capture.
#+BEGIN_SRC emacs-lisp
(package! doct
:recipe (:host github :repo "progfolio/doct")
2020-07-25 05:07:46 +00:00
:pin "80d291e5f1cbdabd4eb7f88c917653c59d3f14be")
#+END_SRC
2020-04-12 05:49:54 +00:00
**** Visuals
2020-02-16 06:13:35 +00:00
Org tables aren't the prettiest thing to look at. This package is supposed to
redraw them in the buffer with box-drawing characters. Sounds like an
improvement to me! Just need to get it working...
2020-02-03 04:57:08 +00:00
#+BEGIN_SRC emacs-lisp
2020-02-16 06:13:35 +00:00
(package! org-pretty-table-mode
:recipe (:host github :repo "Fuco1/org-pretty-table") :pin "88380f865a79bba49e4f501b7fe73a7bfb03bd1a")
2020-02-16 06:13:35 +00:00
#+END_SRC
2020-04-12 05:49:54 +00:00
For automatically toggling LaTeX fragment previews there's this nice package
#+BEGIN_SRC emacs-lisp
2020-07-25 05:07:46 +00:00
(package! org-fragtog :pin "92119e3ae7c9a0ae2b5c9d9e4801b5fdc4804ad7")
2020-04-12 05:49:54 +00:00
#+END_SRC
2020-05-01 12:52:00 +00:00
~org-superstar-mode~ is great. While we're at it we may as well make tags prettier as well :)
2020-04-12 05:49:54 +00:00
#+BEGIN_SRC emacs-lisp
(package! org-pretty-tags :pin "40fd72f3e701e31813f383fb429d30bb88cee769")
2020-04-12 05:49:54 +00:00
#+END_SRC
**** Extra functionality
2020-02-16 06:13:35 +00:00
Because of the /[[https://github.com/commonmark/commonmark-spec/wiki/markdown-flavors][lovely variety in markdown implementations]]/ there isn't actually
such a thing a standard table spec ... or standard anything really. Because
~org-md~ is a goody-two-shoes, it just uses HTML for all these non-standardised
elements (a lot of them). So ~ox-gfm~ is handy for exporting markdown with all the
features that GitHub has. Initialised in [[Exporting to GFM]].
#+BEGIN_SRC emacs-lisp
(package! ox-gfm :pin "99f93011b069e02b37c9660b8fcb45dab086a07f")
2020-02-16 06:13:35 +00:00
#+END_SRC
2020-02-26 11:05:41 +00:00
Now and then citations need to happen
#+BEGIN_SRC emacs-lisp
2020-07-25 05:07:46 +00:00
(package! org-ref :pin "9a8053f0b0e799b8edbae2376dda44704054725a")
2020-02-26 11:05:41 +00:00
#+END_SRC
2020-02-16 06:13:35 +00:00
Came across this and ... it's cool
#+BEGIN_SRC emacs-lisp
(package! org-graph-view :recipe (:host github :repo "alphapapa/org-graph-view") :pin "13314338d70d2c19511efccc491bed3ca0758170")
2020-02-03 04:57:08 +00:00
#+END_SRC
I *need* this in my life. It take a URL to a recipe from a common site, and
inserts an org-ified version at point. Isn't that just great.
#+BEGIN_SRC emacs-lisp
2020-07-25 05:07:46 +00:00
(package! org-chef :pin "77f97ad07b957d6b5ea6c2a57a9e623f2ab36d04")
#+END_SRC
2020-05-15 10:07:10 +00:00
Sometimes I'm given non-org files, that's very sad. Luckily Pandoc offers a way
to make that right again, and this package makes that even easier to do.
2020-05-15 10:07:10 +00:00
#+BEGIN_SRC emacs-lisp
(package! org-pandoc-import :recipe
(:local-repo "lisp/org-pandoc-import" :files ("*.el" "filters" "preprocessors")))
#+END_SRC
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package! org-pandoc-import
:after org)
2020-05-15 10:07:10 +00:00
#+END_SRC
2020-06-29 19:12:22 +00:00
Org-roam is nice by itself, but there are so /extra/ nice packages which integrate
with it.
#+BEGIN_SRC emacs-lisp
2020-07-25 05:07:46 +00:00
(package! org-roam-server :pin "bfc70327413948fb7e631f4424fb92c3dbdeafc0")
2020-06-29 19:12:22 +00:00
#+END_SRC
#+BEGIN_SRC emacs-lisp :tangle yes
(use-package org-roam-server
:after org-roam
:config
(setq org-roam-server-host "127.0.0.1"
org-roam-server-port 8078
org-roam-server-export-inline-images t
org-roam-server-authenticate nil
org-roam-server-label-truncate t
org-roam-server-label-truncate-length 60
org-roam-server-label-wrap-length 20)
(defun org-roam-server-open ()
"Ensure the server is active, then open the roam graph."
(interactive)
(org-roam-server-mode 1)
(browse-url-xdg-open (format "http://localhost:%d" org-roam-server-port))))
#+END_SRC
2020-05-18 16:01:23 +00:00
*** Systemd
For editing systemd unit files
#+BEGIN_SRC emacs-lisp
(package! systemd :pin "51c148e09a129ddf33d95276aa0e89d4ef6f8dd2")
2020-05-18 16:01:23 +00:00
#+END_SRC
* Package configuration
** Abbrev mode
Thanks to [[https://emacs.stackexchange.com/questions/45462/use-a-single-abbrev-table-for-multiple-modes/45476#45476][use a single abbrev-table for multiple modes? - Emacs Stack Exchange]] I
have the following.
#+BEGIN_SRC emacs-lisp
(use-package abbrev
:init
(setq-default abbrev-mode t)
;; a hook funtion that sets the abbrev-table to org-mode-abbrev-table
;; whenever the major mode is a text mode
(defun tec/set-text-mode-abbrev-table ()
(if (derived-mode-p 'text-mode)
(setq local-abbrev-table org-mode-abbrev-table)))
:commands abbrev-mode
:hook
(abbrev-mode . tec/set-text-mode-abbrev-table)
:config
(setq abbrev-file-name (expand-file-name "abbrev.el" doom-private-dir))
(setq save-abbrevs 'silently))
#+END_SRC
2020-04-29 08:47:48 +00:00
** Calc
Radians are just better
#+BEGIN_SRC emacs-lisp
(setq calc-angle-mode 'rad ;; radians are rad
calc-algebraic-mode t ;; allows '2*x instead of 'x<RET>2*
calc-symbolic-mode t) ;; keeps stuff like √2 irrational for as long as possible
(after! calctex
(setq calctex-format-latex-header (concat calctex-format-latex-header
"\n\\usepackage{arevmath}")))
(add-hook 'calc-mode-hook #'calctex-mode)
#+END_SRC
** Centaur Tabs
We want to make the tabs a nice, comfy size (~36~), with icons. The modifier
marker is nice, but the particular default Unicode one causes a lag spike, so
let's just switch to an ~o~, which still looks decent but doesn't cause any
issues.
A 'active-bar' is nice, so let's have one of those. If we have it ~under~ needs us to
turn on ~x-underline-at-decent~ though. For some reason this didn't seem to work
inside the ~(after! ... )~ block ¯\_(ツ)_/¯.
Then let's change the font to a sans serif, but the default one doesn't fit too
well somehow, so let's switch to 'P22 Underground Book'; it looks much nicer.
#+BEGIN_SRC emacs-lisp
(after! centaur-tabs
2020-04-09 18:29:06 +00:00
(centaur-tabs-mode -1)
(setq centaur-tabs-height 36
centaur-tabs-set-icons t
centaur-tabs-modified-marker "o"
2020-01-09 18:02:20 +00:00
centaur-tabs-close-button "×"
centaur-tabs-set-bar 'above)
centaur-tabs-gray-out-icons 'buffer
(centaur-tabs-change-fonts "P22 Underground Book" 160))
;; (setq x-underline-at-descent-line t)
#+END_SRC
** Company
It's nice to have completions almost all the time, in my opinion. Key strokes
are just waiting to be saved!
#+BEGIN_SRC emacs-lisp
(after! company
(setq company-idle-delay 0.5
company-minimum-prefix-length 2)
2020-02-26 12:49:02 +00:00
(setq company-show-numbers t)
(add-hook 'evil-normal-state-entry-hook #'company-abort)) ;; make aborting less annoying.
#+END_SRC
2020-07-19 16:33:28 +00:00
Now, the improvements from ~precedent~ are mostly from remembering history, so
let's improve that memory.
#+BEGIN_SRC emacs-lisp
(setq-default history-length 1000)
(setq-default prescient-history-length 1000)
#+END_SRC
*** Plain Text
2020-07-19 16:33:28 +00:00
~Ispell~ is nice, let's have it in ~text~, ~markdown~, and ~GFM~.
#+BEGIN_SRC emacs-lisp
(set-company-backend! '(text-mode
markdown-mode
gfm-mode)
'(:seperate company-ispell
company-files
company-yasnippet))
#+END_SRC
2020-07-19 16:33:28 +00:00
We then configure the dictionary we're using in [[*Ispell][Ispell]].
*** ESS
~company-dabbrev-code~ is nice. Let's have it.
#+BEGIN_SRC emacs-lisp
(set-company-backend! 'ess-r-mode '(company-R-args company-R-objects company-dabbrev-code :separate))
#+END_SRC
2020-08-08 09:11:05 +00:00
** Circe
Circe is a client for IRC in Emacs (hey, isn't that a nice project
name+acronym), and a greek enchantress who turned humans into animals.
Let's use the former to chat to +recluses+ discerning individuals online.
2020-08-08 08:40:19 +00:00
[[xkcd:1782]]
2020-08-08 09:11:05 +00:00
Before we start seeing and sending messages, we need to authenticate with our
IRC servers. The circe manual provided a snippet for putting some of the auth
details in =.authinfo.gpg= --- but I think we should go further than that: have
the entire server info in our authinfo.
First, a reasonable format by which we can specify:
+ server
+ port
+ SASL username
+ SASL password
+ channels to join
We can have these stored like so
#+BEGIN_SRC authinfo
machine chat.freenode.net login USERNAME password PASSWORD port PORT for irc channels emacs,org-mode
#+END_SRC
The ~for irc~ bit is used so we can uniquely identify all IRC auth info. By
omitting the =#= in channel names we can have a list of channels comma-separated
(no space!) which the secrets API will return as a single string.
#+NAME: irc-authinfo-reader
#+BEGIN_SRC emacs-lisp :tangle no
2020-08-08 08:40:19 +00:00
(defun auth-server-pass (server)
(if-let ((secret (plist-get (car (auth-source-search :host server)) :secret)))
(if (functionp secret)
(funcall secret) secret)
(error "Could not fetch password for host %s" server)))
(defun register-irc-auths ()
(require 'circe)
(require 'dash)
(let ((accounts (-filter (lambda (a) (string= "irc" (plist-get a :for)))
(auth-source-search :require '(:for) :max 10))))
(appendq! circe-network-options
(mapcar (lambda (entry)
(let* ((host (plist-get entry :host))
(label (or (plist-get entry :label) host))
(_ports (mapcar #'string-to-number
2020-08-08 09:11:05 +00:00
(s-split "," (plist-get entry :port))))
2020-08-08 08:40:19 +00:00
(port (if (= 1 (length _ports)) (car _ports) _ports))
(user (plist-get entry :user))
(nick (or (plist-get entry :nick) user))
(channels (mapcar (lambda (c) (concat "#" c))
(s-split "," (plist-get entry :channels)))))
`(,label
:host ,host :port ,port :nick ,nick
:sasl-username ,user :sasl-password auth-server-pass
:channels ,channels)))
accounts))))
2020-08-08 09:11:05 +00:00
#+END_SRC
2020-08-08 08:40:19 +00:00
2020-08-08 09:11:05 +00:00
We'll just call ~(register-irc-auths)~ on a hook when we start Circe up.
Now we're ready to go, let's actually wire-up Circe, with one or two
configuration tweaks.
#+BEGIN_SRC emacs-lisp :noweb yes
2020-08-08 08:40:19 +00:00
(after! circe
(setq-default circe-use-tls t)
(setq circe-notifications-alert-icon "/usr/share/icons/breeze/actions/24/network-connect.svg"
lui-logging-directory "~/.emacs.d/.local/etc/irc"
lui-logging-file-format "{buffer}/%Y/%m-%d.txt"
circe-format-self-say "{nick:+13s} ┃ {body}")
(custom-set-faces!
'(circe-my-message-face :weight unspecified))
(enable-lui-logging-globally)
(enable-circe-display-images)
<<org-emph-to-irc>>
<<circe-emojis>>
<<circe-emoji-alists>>
(defun named-circe-prompt ()
(lui-set-prompt
(concat (propertize (format "%13s > " (circe-nick))
'face 'circe-prompt-face)
"")))
(add-hook 'circe-chat-mode-hook #'named-circe-prompt)
(appendq! all-the-icons-mode-icon-alist
'((circe-channel-mode all-the-icons-material "message" :face all-the-icons-lblue)
(circe-server-mode all-the-icons-material "chat_bubble_outline" :face all-the-icons-purple))))
2020-08-08 09:11:05 +00:00
<<irc-authinfo-reader>>
2020-08-08 08:40:19 +00:00
(add-transient-hook! #'=irc (register-irc-auths))
#+END_SRC
*** Org-style emphasis
Let's do our *bold*, /italic/, and _underline_ in org-syntax, using IRC control charachters
#+name: org-emph-to-irc
#+BEGIN_SRC emacs-lisp
(defun lui-org-to-irc ()
"Examine a buffer with simple org-mode formatting, and converts the empasis:
,*bold*, /italic/, and _underline_ to IRC semi-standard escape codes.
=code= is converted to inverse (highlighted) text."
(goto-char (point-min))
(while (re-search-forward "\\_<\\(?1:[*/_=]\\)\\(?2:[^[:space:]]\\(?:.*?[^[:space:]]\\)?\\)\\1\\_>" nil t)
(replace-match
(concat (pcase (match-string 1)
("*" "")
("/" "")
("_" "")
("=" ""))
(match-string 2)
"") nil nil)))
(add-hook 'lui-pre-input-hook #'lui-org-to-irc)
#+END_SRC
*** Emojis
Let's setup Circe to use some emojis
#+name: circe-emojis
#+BEGIN_SRC emacs-lisp :tangle no
(defun lui-ascii-to-emoji ()
(goto-char (point-min))
(while (re-search-forward "\\( \\)?::?\\([^[:space:]:]+\\):\\( \\)?" nil t)
(replace-match
(concat
(match-string 1)
(or (cdr (assoc (match-string 2) lui-emojis-alist))
(concat ":" (match-string 2) ":"))
(match-string 3))
nil nil)))
(defun lui-emoticon-to-emoji ()
(dolist (emoticon lui-emoticons-alist)
(goto-char (point-min))
(while (re-search-forward (concat " " (car emoticon) "\\( \\)?") nil t)
(replace-match (concat " "
(cdr (assoc (cdr emoticon) lui-emojis-alist))
(match-string 1))))))
(define-minor-mode lui-emojify
"Replace :emojis: and ;) emoticons with unicode emoji chars."
:global t
:init-value t
(if lui-emojify
(add-hook! lui-pre-input #'lui-ascii-to-emoji #'lui-emoticon-to-emoji)
(remove-hook! lui-pre-input #'lui-ascii-to-emoji #'lui-emoticon-to-emoji)))
#+END_SRC
Now, some actual emojis to use.
#+NAME: circe-emoji-alists
#+BEGIN_SRC emacs-lisp :tangle no
(defvar lui-emojis-alist
'(("grinning" . "😀")
("smiley" . "😃")
("smile" . "😄")
("grin" . "😁")
("laughing" . "😆")
("sweat_smile" . "😅")
("joy" . "😂")
("rofl" . "🤣")
("relaxed" . "☺️")
("blush" . "😊")
("innocent" . "😇")
("slight_smile" . "🙂")
("upside_down" . "🙃")
("wink" . "😉")
("relieved" . "😌")
("heart_eyes" . "😍")
("yum" . "😋")
("stuck_out_tongue" . "😛")
("stuck_out_tongue_closed_eyes" . "😝")
("stuck_out_tongue_wink" . "😜")
("zanzy" . "🤪")
("raised_eyebrow" . "🤨")
("monocle" . "🧐")
("nerd" . "🤓")
("cool" . "😎")
("star_struck" . "🤩")
("party" . "🥳")
("smirk" . "😏")
("unamused" . "😒")
("disapointed" . "😞")
("pensive" . "😔")
("worried" . "😟")
("confused" . "😕")
("slight_frown" . "🙁")
("frown" . "☹️")
("persevere" . "😣")
("confounded" . "😖")
("tired" . "😫")
("weary" . "😩")
("pleading" . "🥺")
("tear" . "😢")
("cry" . "😢")
("sob" . "😭")
("triumph" . "😤")
("angry" . "😠")
("rage" . "😡")
("exploding_head" . "🤯")
("flushed" . "😳")
("hot" . "🥵")
("cold" . "🥶")
("scream" . "😱")
("fearful" . "😨")
("disapointed" . "😰")
("relieved" . "😥")
("sweat" . "😓")
("thinking" . "🤔")
("shush" . "🤫")
("liar" . "🤥")
("blank_face" . "😶")
("neutral" . "😐")
("expressionless" . "😑")
("grimace" . "😬")
("rolling_eyes" . "🙄")
("hushed" . "😯")
("frowning" . "😦")
("anguished" . "😧")
("wow" . "😮")
("astonished" . "😲")
("sleeping" . "😴")
("drooling" . "🤤")
("sleepy" . "😪")
("dizzy" . "😵")
("zipper_mouth" . "🤐")
("woozy" . "🥴")
("sick" . "🤢")
("vomiting" . "🤮")
("sneeze" . "🤧")
("mask" . "😷")
("bandaged_head" . "🤕")
("money_face" . "🤑")
("cowboy" . "🤠")
("imp" . "😈")
("ghost" . "👻")
("alien" . "👽")
("robot" . "🤖")
("clap" . "👏")
("thumpup" . "👍")
("+1" . "👍")
("thumbdown" . "👎")
("-1" . "👎")
("ok" . "👌")
("pinch" . "🤏")
("left" . "👈")
("right" . "👉")
("down" . "👇")
("wave" . "👋")
("pray" . "🙏")
("eyes" . "👀")
("brain" . "🧠")
("facepalm" . "🤦")
("tada" . "🎉")
("fire" . "🔥")
("flying_money" . "💸")
("lighbulb" . "💡")
("heart" . "❤️")
("sparkling_heart" . "💖")
("heartbreak" . "💔")
("100" . "💯")))
(defvar lui-emoticons-alist
'((":)" . "slight_smile")
(";)" . "wink")
(":D" . "smile")
("=D" . "grin")
("xD" . "laughing")
(";(" . "joy")
(":P" . "stuck_out_tongue")
(";D" . "stuck_out_tongue_wink")
("xP" . "stuck_out_tongue_closed_eyes")
(":(" . "slight_frown")
(";(" . "cry")
(";'(" . "sob")
(">:(" . "angry")
(">>:(" . "rage")
(":o" . "wow")
(":O" . "astonished")
(":/" . "confused")
(":-/" . "thinking")
(":|" . "neutral")
(":-|" . "expressionless")))
#+END_SRC
2020-04-29 08:47:48 +00:00
** Elcord
#+BEGIN_SRC emacs-lisp
(setq elcord-use-major-mode-as-main-icon t)
#+END_SRC
** [[https://github.com/zachcurry/emacs-anywhere][Emacs Anywhere]] configuration
2020-07-25 05:54:20 +00:00
To start with, let's install this.
#+BEGIN_SRC shell :tangle (if (executable-find "emacs_anywhere") "no" "setup.sh")
2020-07-25 05:54:20 +00:00
cd /tmp
curl -fsSL https://raw.github.com/zachcurry/emacs-anywhere/master/install -o ea-install.sh
sed -i 's/EA_PATH=$HOME\/.emacs_anywhere/EA_PATH=$HOME\/.local\/share\/emacs_anywhere/' ea-install.sh
bash ea-install.sh || exit
cd ~/.local/share/emacs_anywhere
# Install in ~/.local not ~/.emacs_anywhere
sed -i 's/$HOME\/.emacs_anywhere/$HOME\/.local\/share\/emacs_anywhere/' ./bin/linux ./bin/emacstask
ln -s ~/.local/share/emacs_anywhere/bin/linux ~/.local/bin/emacs_anywhere
# Improve paste robustness --- https://github.com/zachcurry/emacs-anywhere/pull/66
sed -i 's/xdotool key --clearmodifiers ctrl+v/xdotool key --clearmodifiers Shift+Insert/' ./bin/linux
#+END_SRC
It's nice to recognise GitHub (so we can use ~GFM~), and other apps which we know
take markdown
#+BEGIN_SRC emacs-lisp
(defun markdown-window-p (window-title)
"Judges from WINDOW-TITLE whether the current window likes markdown"
(if (string-match-p (rx (or "Stack Exchange" "Stack Overflow"
"Pull Request" "Issue" "Discord"))
window-title) t nil))
#+END_SRC
When the window opens, we generally want text so let's use a nice sans serif font,
a position the window below and to the left. Oh, and don't forget about checking
for ~GFM~, otherwise let's just use ~markdown~.
#+BEGIN_SRC emacs-lisp
(defvar emacs-anywhere--active-markdown nil
"Whether the buffer started off as markdown.
Affects behaviour of `emacs-anywhere--finalise-content'")
(defun emacs-anywhere--finalise-content (&optional _frame)
(when emacs-anywhere--active-markdown
(fundamental-mode)
(goto-char (point-min))
(insert "#+OPTIONS: toc:nil\n")
(rename-buffer "*EA Pre Export*")
(org-export-to-buffer 'gfm ea--buffer-name)
(kill-buffer "*EA Pre Export*"))
(gui-select-text (buffer-string)))
(define-minor-mode emacs-anywhere-mode
"To tweak the current buffer for some emacs-anywhere considerations"
:init-value nil
:keymap (list
;; Finish edit, but be smart in org mode
(cons (kbd "C-c C-c") (cmd! (if (and (eq major-mode 'org-mode)
(org-in-src-block-p))
(org-ctrl-c-ctrl-c)
(delete-frame))))
;; Abort edit. emacs-anywhere saves the current edit for next time.
(cons (kbd "C-c C-k") (cmd! (setq ea-on nil)
(delete-frame))))
(when emacs-anywhere-mode
2020-03-24 04:45:07 +00:00
;; line breaking
(turn-off-auto-fill)
(visual-line-mode t)
;; DEL/C-SPC to clear (first keystroke only)
(set-transient-map (let ((keymap (make-sparse-keymap)))
(define-key keymap (kbd "DEL") (cmd! (delete-region (point-min) (point-max))))
(define-key keymap (kbd "C-SPC") (cmd! (delete-region (point-min) (point-max))))
keymap))
;; disable tabs
(when (bound-and-true-p centaur-tabs-mode)
(centaur-tabs-local-mode t))))
(defun ea-popup-handler (app-name window-title x y w h)
(interactive)
(set-frame-size (selected-frame) 80 12)
;; position the frame near the mouse
(let* ((mousepos (split-string (shell-command-to-string "xdotool getmouselocation | sed -E \"s/ screen:0 window:[^ ]*|x:|y://g\"")))
(mouse-x (- (string-to-number (nth 0 mousepos)) 100))
(mouse-y (- (string-to-number (nth 1 mousepos)) 50)))
(set-frame-position (selected-frame) mouse-x mouse-y))
(set-frame-name (concat "Quick Edit ∷ " ea-app-name " — "
(truncate-string-to-width
(string-trim
(string-trim-right window-title
(format "-[A-Za-z0-9 ]*%s" ea-app-name))
"[\s-]+" "[\s-]+")
45 nil nil "…")))
(message "window-title: %s" window-title)
(when-let ((selection (gui-get-selection 'PRIMARY)))
(insert selection))
(setq emacs-anywhere--active-markdown (markdown-window-p window-title))
;; convert buffer to org mode if markdown
(when emacs-anywhere--active-markdown
(shell-command-on-region (point-min) (point-max)
"pandoc -f markdown -t org" nil t)
(deactivate-mark) (goto-char (point-max)))
;; set major mode
(org-mode)
(advice-add 'ea--delete-frame-handler :before #'emacs-anywhere--finalise-content)
;; I'll be honest with myself, I /need/ spellcheck
(flyspell-buffer)
(evil-insert-state) ; start in insert
(emacs-anywhere-mode 1))
(add-hook 'ea-popup-hook 'ea-popup-handler)
#+END_SRC
2020-04-29 08:47:48 +00:00
** Eros-eval
This makes the result of evals with =gr= and =gR= just slightly prettier. Every bit
counts right?
#+BEGIN_SRC emacs-lisp
(setq eros-eval-result-prefix "⟹ ")
#+END_SRC
** EVIL
I don't use ~evil-escape-mode~, so I may as well turn it off, I've heard it
contributes a typing delay. I'm not sure it's much, but it is an extra
~pre-command-hook~ that I don't benefit from, so...
#+BEGIN_SRC emacs-lisp
(after! evil (evil-escape-mode nil))
#+END_SRC
** Flyspell
At one point, typing became noticeably laggy, Profiling revealed
~flyspell-post-command-hook~ was responsible for 47% of CPU cycles by itself!
So I'm going to make use of ~flyspell-lazy~
#+BEGIN_SRC emacs-lisp
(after! flyspell (require 'flyspell-lazy) (flyspell-lazy-mode 1))
#+END_SRC
2020-05-16 09:34:29 +00:00
** Info colors
#+BEGIN_SRC emacs-lisp
(use-package! info-colors
:commands (info-colors-fontify-node))
(add-hook 'Info-selection-hook 'info-colors-fontify-node)
(add-hook 'Info-mode-hook #'mixed-pitch-mode)
#+END_SRC
2020-04-29 08:47:48 +00:00
** Ispell
2020-06-29 19:11:44 +00:00
*** Downloading dictionaries
2020-04-29 08:47:48 +00:00
Let's get a nice big dictionary from [[http://app.aspell.net/create][SCOWL Custom List/Dictionary Creator]] with
the following configuration
- size :: 80 (huge)
- spellings :: British(-ise) and Australian
- spelling variants level :: 0
- diacritics :: keep
- extra lists :: hacker, roman numerals
2020-06-29 19:11:44 +00:00
**** Hunspell
#+BEGIN_SRC shell :tangle (if (file-exists-p "/usr/share/myspell/en-custom.dic") "no" "setup.sh")
2020-06-29 19:11:44 +00:00
cd /tmp
curl -o "hunspell-en-custom.zip" 'http://app.aspell.net/create?max_size=80&spelling=GBs&spelling=AU&max_variant=0&diacritic=keep&special=hacker&special=roman-numerals&encoding=utf-8&format=inline&download=hunspell'
unzip "hunspell-en-custom.zip"
sudo chown root:root en-custom.*
sudo mv en-custom.{aff,dic} /usr/share/myspell/
#+END_SRC
**** Aspell
#+BEGIN_SRC shell :tangle (if (file-expand-wildcards "/usr/lib64/aspell*/en-custom.multi") "no" "setup.sh")
2020-06-29 19:11:44 +00:00
cd /tmp
curl -o "aspell6-en-custom.tar.bz2" 'http://app.aspell.net/create?max_size=80&spelling=GBs&spelling=AU&max_variant=0&diacritic=keep&special=hacker&special=roman-numerals&encoding=utf-8&format=inline&download=aspell'
tar -xjf "aspell6-en-custom.tar.bz2"
cd aspell6-en-custom
./configure && make && sudo make install
#+END_SRC
*** Configuration
2020-04-29 08:47:48 +00:00
#+BEGIN_SRC emacs-lisp
2020-06-29 19:11:44 +00:00
(setq ispell-dictionary "en-custom")
2020-04-29 08:47:48 +00:00
#+END_SRC
Oh, and by the way, if ~company-ispell-dictionary~ is ~nil~, then
~ispell-complete-word-dict~ is used instead, which once again when ~nil~ is
~ispell-alternate-dictionary~, which at the moment maps to a plaintext version of
the above.
It seems reasonable to want to keep an eye on my personal dict, let's have it
nearby (also means that if I change the 'main' dictionary I keep my addition).
#+BEGIN_SRC emacs-lisp
2020-06-29 19:11:44 +00:00
(setq ispell-personal-dictionary (expand-file-name ".ispell_personal" doom-private-dir))
2020-04-29 08:47:48 +00:00
#+END_SRC
** Ivy
While in an ivy mini-buffer =C-o= shows a list of all possible actions one may take.
By default this is ~#'ivy-read-action-by-key~ however a better interface to this
is using Hydra.
#+BEGIN_SRC emacs-lisp
(setq ivy-read-action-function #'ivy-hydra-read-action)
#+END_SRC
I currently have ~40k functions. This seems like sufficient motivation to
increase the maximum number of items ivy will sort to 40k + a bit, this way
=SPC h f= et al. will continue to function as expected.
#+BEGIN_SRC emacs-lisp
(setq ivy-sort-max-size 50000)
#+END_SRC
2020-04-29 10:33:15 +00:00
** Magit
Magit is pretty nice by default. The diffs don't get any
syntax-highlighting-love though which is a bit sad. Thankfully
[[https://github.com/dandavison/magit-delta][dandavison/magit-delta]] exists, which we can put to use.
#+BEGIN_SRC emacs-lisp
;; (after! magit
;; (magit-delta-mode +1))
#+END_SRC
Unfortunately this seems to mess things up, which is something I'll want to look
into later.
2020-05-23 04:17:08 +00:00
** Mail
[[xkcd:1467]]
*** Fetching
The contenders for this seem to be:
+ [[https://www.offlineimap.org/][OfflineIMAP]] ([[https://wiki.archlinux.org/index.php/OfflineIMAP][ArchWiki page]])
+ [[http://isync.sourceforge.net/mbsync.html][isync/mbsync]] ([[https://wiki.archlinux.org/index.php/isync][ArchWiki page]])
From perusing r/emacs the prevailing opinion seems to be that
+ isync is faster
+ isync works more reliably
So let's use that.
The config was straightforward, and is located at [[file:~/.mbsyncrc][~/.mbsyncrc]].
I'm currently successfully connecting to: Gmail, office365mail, and dovecot.
I'm also shoving passwords in my [[file:~/.authinfo.gpg][authinfo.gpg]] and fetching them using ~PassCmd~:
#+BEGIN_SRC shell :tangle no :eval no
gpg2 -q --for-your-eyes-only --no-tty -d ~/.authinfo.gpg | awk '/machine IMAP_SERCER login EMAIL_ADDR/ {print $NF}'
#+END_SRC
We can run ~mbsync -a~ in a systemd service file or something, but we can do
better than that. [[https://github.com/vsemyonoff/easymail#usage][vsemyonoff/easymail]] seems like the sort of thing we want, but
is written for =notmuch= unfortunately. We can still use it for inspiration though.
Using [[https://gitlab.com/shackra/goimapnotify][goimapnotify]] we should be able to sync just after new
mail. Unfortunately this means /yet another/ config file :(
We install with
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "goimapnotify") "no" "setup.sh")
2020-05-23 04:17:08 +00:00
go get -u gitlab.com/shackra/goimapnotify
2020-06-29 19:03:37 +00:00
ln -s ~/.local/share/go/bin/goimapnotify ~/.local/bin/
2020-05-23 04:17:08 +00:00
#+END_SRC
Here's the general plan:
1. Use ~goimapnotify~ to monitor mailboxes
This needs it's own set of configs, and =systemd= services, which is a pain. We
remove this pain by writing a python script (found below) to setup these
config files, and systemd services by parsing the [[file:~/.mbsyncrc][~/.mbsyncrc]] file.
2. On new mail, call ~mbsync --pull --new ACCOUNT:BOX~
We try to be as specific as possible, so ~mbsync~ returns as soon as possible,
and we can /get those emails as soon as possible/.
3. Try to call ~mu index --lazy-fetch~.
This fails if mu4e is already open (due to a write lock on the database), so
in that case we just ~touch~ a tmp file (=/tmp/mu_reindex_now=).
4. Separately, we set up Emacs to check for the existance of
=/tmp/mu_reindex_now= once a second while mu4e is
running, and (after deleting the file) call ~mu4e-update-index~.
Let's start off by handling the elisp side of things
**** Rebuild mail index while using mu4e
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defvar mu4e-reindex-request-file "/tmp/mu_reindex_now"
"Location of the reindex request, signaled by existance")
(defvar mu4e-reindex-request-min-seperation 5.0
2020-05-23 04:17:08 +00:00
"Don't refresh again until this many second have elapsed.
Prevents a series of redisplays from being called (when set to an appropriate value)")
(defvar mu4e-reindex-request--file-watcher nil)
(defvar mu4e-reindex-request--file-just-deleted nil)
(defvar mu4e-reindex-request--last-time 0)
(defun mu4e-reindex-request--add-watcher ()
(setq mu4e-reindex-request--file-just-deleted nil)
(setq mu4e-reindex-request--file-watcher
(file-notify-add-watch mu4e-reindex-request-file
'(change)
#'mu4e-file-reindex-request)))
(defadvice! mu4e-stop-watching-for-reindex-request ()
:after #'mu4e~proc-kill
(if mu4e-reindex-request--file-watcher
(file-notify-rm-watch mu4e-reindex-request--file-watcher)))
(defadvice! mu4e-watch-for-reindex-request ()
:after #'mu4e~proc-start
(mu4e-stop-watching-for-reindex-request)
(when (file-exists-p mu4e-reindex-request-file)
(delete-file mu4e-reindex-request-file))
(mu4e-reindex-request--add-watcher))
(defun mu4e-file-reindex-request (event)
"Act based on the existance of `mu4e-reindex-request-file'"
(if mu4e-reindex-request--file-just-deleted
(mu4e-reindex-request--add-watcher)
(when (equal (nth 1 event) 'created)
(delete-file mu4e-reindex-request-file)
(setq mu4e-reindex-request--file-just-deleted t)
(mu4e-reindex-maybe t))))
(defun mu4e-reindex-maybe (&optional new-request)
"Run `mu4e~proc-index' if it's been more than `mu4e-reindex-request-min-seperation' seconds since the last request,"
(let ((time-since-last-request (- (float-time) mu4e-reindex-request--last-time)))
(when new-request
(setq mu4e-reindex-request--last-time (float-time)))
(if (> time-since-last-request mu4e-reindex-request-min-seperation)
(mu4e~proc-index nil t)
(when new-request
(run-at-time (* 1.1 mu4e-reindex-request-min-seperation) nil
#'mu4e-reindex-maybe))))))
#+END_SRC
**** Config transcoding & service management
As long as the =mbsyncrc= file exists, this is as easy as running
#+BEGIN_SRC shell :tangle (if (file-exists-p "~/.imapnotify") "no" "setup.sh")
2020-05-23 04:17:08 +00:00
~/.config/doom/misc/mbsync-imapnotify.py
#+END_SRC
When run without flags this will perform the following actions
+ Read, and parse [[file:~/.mbsyncrc][~/.mbsyncrc]], specifically recognising the following properties
- ~IMAPAccount~
- ~Host~
- ~Port~
- ~User~
- ~Password~
- ~PassCmd~
- ~Patterns~
+ Call ~mbsync --list ACCOUNT~, and filter results according to ~Patterns~
+ Construct a imapnotify config for each account, with the following hooks
- onNewMail :: ~mbsync --pull ACCOUNT:MAILBOX~
- onNewMailPost :: ~if mu index --lazy-check; then test -f /tmp/mu_reindex_now && rm /tmp/mu_reindex_now; else touch /tmp/mu_reindex_now; fi~
+ Compare accounts list to previous accounts, enable/disable the relevant
systemd services, called with the ~--now~ flag (start/stop services as well)
This script also supports the following flags
+ ~--status~ to get the status of the relevant systemd services supports =active=,
=failing=, and =disabled=
+ ~--enable~ to enable all relevant systemd services
+ ~--disable~ to disable all relevant systemd services
#+BEGIN_SRC python :tangle misc/mbsync-imapnotify.py :shebang "#!/usr/bin/env python3"
from pathlib import Path
import json
import re
import shutil
import subprocess
import sys
import fnmatch
mbsyncFile = Path("~/.mbsyncrc").expanduser()
imapnotifyConfigFolder = Path("~/.imapnotify/").expanduser()
imapnotifyConfigFolder.mkdir(exist_ok=True)
imapnotifyConfigFilename = "notify.conf"
imapnotifyDefault = {
"host": "",
"port": 993,
"tls": True,
"tlsOptions": {"rejectUnauthorized": True},
"onNewMail": "",
"onNewMailPost": "if mu index --lazy-check; then test -f /tmp/mu_reindex_now && rm /tmp/mu_reindex_now; else touch /tmp/mu_reindex_now; fi",
}
def stripQuotes(string):
if string[0] == '"' and string[-1] == '"':
return string[1:-1].replace('\\"', '"')
mbsyncInotifyMapping = {
"Host": (str, "host"),
"Port": (int, "port"),
"User": (str, "username"),
"Password": (str, "password"),
"PassCmd": (stripQuotes, "passwordCmd"),
"Patterns": (str, "_patterns"),
}
oldAccounts = [d.name for d in imapnotifyConfigFolder.iterdir() if d.is_dir()]
currentAccount = ""
currentAccountData = {}
successfulAdditions = []
def processLine(line):
newAcc = re.match(r"^IMAPAccount ([^#]+)", line)
linecontent = re.sub(r"(^|[^\\])#.*", "", line).split(" ", 1)
if len(linecontent) != 2:
return
parameter, value = linecontent
if parameter == "IMAPAccount":
if currentAccountNumber > 0:
finaliseAccount()
newAccount(value)
elif parameter in mbsyncInotifyMapping.keys():
parser, key = mbsyncInotifyMapping[parameter]
currentAccountData[key] = parser(value)
elif parameter == "Channel":
currentAccountData["onNewMail"] = f"mbsync --pull --new {value}:'%s'"
def newAccount(name):
global currentAccountNumber
global currentAccount
global currentAccountData
currentAccountNumber += 1
currentAccount = name
currentAccountData = {}
print(f"\n\033[1;32m{currentAccountNumber}\033[0;32m - {name}\033[0;37m")
def accountToFoldername(name):
return re.sub(r"[^A-Za-z0-9]", "", name)
def finaliseAccount():
if currentAccountNumber == 0:
return
global currentAccountData
try:
currentAccountData["boxes"] = getMailBoxes(currentAccount)
except subprocess.CalledProcessError as e:
print(
f"\033[1;31mError:\033[0;31m failed to fetch mailboxes (skipping): "
+ f"`{' '.join(e.cmd)}' returned code {e.returncode}\033[0;37m"
)
return
except subprocess.TimeoutExpired as e:
print(
f"\033[1;31mError:\033[0;31m failed to fetch mailboxes (skipping): "
+ f"`{' '.join(e.cmd)}' timed out after {e.timeout:.2f} seconds\033[0;37m"
2020-05-23 04:17:08 +00:00
)
return
if "_patterns" in currentAccountData:
currentAccountData["boxes"] = applyPatternFilter(
currentAccountData["_patterns"], currentAccountData["boxes"]
)
# strip not-to-be-exported data
currentAccountData = {
k: currentAccountData[k] for k in currentAccountData if k[0] != "_"
}
parametersSet = currentAccountData.keys()
currentAccountData = {**imapnotifyDefault, **currentAccountData}
for key, val in currentAccountData.items():
valColor = "\033[0;33m" if key in parametersSet else "\033[0;37m"
print(f" \033[1;37m{key:<13} {valColor}{val}\033[0;37m")
if (
len(currentAccountData["boxes"]) > 15
and "@gmail.com" in currentAccountData["username"]
):
print(
" \033[1;31mWarning:\033[0;31m Gmail raises an error when more than"
+ "\033[1;31m15\033[0;31m simultanious connections are attempted."
+ "\n You are attempting to monitor "
+ f"\033[1;31m{len(currentAccountData['boxes'])}\033[0;31m mailboxes.\033[0;37m"
)
configFile = (
imapnotifyConfigFolder
/ accountToFoldername(currentAccount)
/ imapnotifyConfigFilename
)
configFile.parent.mkdir(exist_ok=True)
json.dump(currentAccountData, open(configFile, "w"), indent=2)
print(f" \033[0;35mConfig generated and saved to {configFile}\033[0;37m")
global successfulAdditions
successfulAdditions.append(accountToFoldername(currentAccount))
def getMailBoxes(account):
boxes = subprocess.run(
["mbsync", "--list", account], check=True, stdout=subprocess.PIPE, timeout=10.0
)
return boxes.stdout.decode("utf-8").strip().split("\n")
def applyPatternFilter(pattern, mailboxes):
patternRegexs = getPatternRegexes(pattern)
return [m for m in mailboxes if testPatternRegexs(patternRegexs, m)]
def getPatternRegexes(pattern):
def addGlob(b):
blobs.append(b.replace('\\"', '"'))
return ""
blobs = []
pattern = re.sub(r' ?"([^"]+)"', lambda m: addGlob(m.groups()[0]), pattern)
blobs.extend(pattern.split(" "))
blobs = [
(-1, fnmatch.translate(b[1::])) if b[0] == "!" else (1, fnmatch.translate(b))
for b in blobs
]
return blobs
def testPatternRegexs(regexCond, case):
for factor, regex in regexCond:
if factor * bool(re.match(regex, case)) < 0:
return False
return True
def processSystemdServices():
keptAccounts = [acc for acc in successfulAdditions if acc in oldAccounts]
freshAccounts = [acc for acc in successfulAdditions if acc not in oldAccounts]
staleAccounts = [acc for acc in oldAccounts if acc not in successfulAdditions]
if keptAccounts:
print(f"\033[1;34m{len(keptAccounts)}\033[0;34m kept accounts:\033[0;37m")
restartAccountSystemdServices(keptAccounts)
if freshAccounts:
print(f"\033[1;32m{len(freshAccounts)}\033[0;32m new accounts:\033[0;37m")
enableAccountSystemdServices(freshAccounts)
else:
print(f"\033[0;32mNo new accounts.\033[0;37m")
notActuallyEnabledAccounts = [
acc for acc in successfulAdditions if not getAccountServiceState(acc)["enabled"]
]
if notActuallyEnabledAccounts:
print(
f"\033[1;32m{len(notActuallyEnabledAccounts)}\033[0;32m accounts need re-enabling:\033[0;37m"
)
enableAccountSystemdServices(notActuallyEnabledAccounts)
if staleAccounts:
print(f"\033[1;33m{len(staleAccounts)}\033[0;33m removed accounts:\033[0;37m")
disableAccountSystemdServices(staleAccounts)
else:
print(f"\033[0;33mNo removed accounts.\033[0;37m")
def enableAccountSystemdServices(accounts):
for account in accounts:
print(f" \033[0;32m - \033[1;37m{account:<18}", end="\033[0;37m", flush=True)
if setSystemdServiceState(
"enable", f"goimapnotify@{accountToFoldername(account)}.service"
):
print("\033[1;32m enabled")
def disableAccountSystemdServices(accounts):
for account in accounts:
print(f" \033[0;33m - \033[1;37m{account:<18}", end="\033[0;37m", flush=True)
if setSystemdServiceState(
"disable", f"goimapnotify@{accountToFoldername(account)}.service"
):
print("\033[1;33m disabled")
def restartAccountSystemdServices(accounts):
for account in accounts:
print(f" \033[0;34m - \033[1;37m{account:<18}", end="\033[0;37m", flush=True)
if setSystemdServiceState(
"restart", f"goimapnotify@{accountToFoldername(account)}.service"
):
print("\033[1;34m restarted")
def setSystemdServiceState(state, service):
try:
enabler = subprocess.run(
["systemctl", "--user", state, service, "--now"],
check=True,
stderr=subprocess.DEVNULL,
timeout=5.0,
2020-05-23 04:17:08 +00:00
)
return True
except subprocess.CalledProcessError as e:
print(
f" \033[1;31mfailed\033[0;31m to {state}, `{' '.join(e.cmd)}'"
+ f"returned code {e.returncode}\033[0;37m"
)
except subprocess.TimeoutExpired as e:
print(f" \033[1;31mtimed out after {e.timeout:.2f} seconds\033[0;37m")
2020-05-23 04:17:08 +00:00
return False
def getAccountServiceState(account):
return {
state: bool(
1
- subprocess.run(
[
"systemctl",
"--user",
f"is-{state}",
"--quiet",
f"goimapnotify@{accountToFoldername(account)}.service",
],
stderr=subprocess.DEVNULL,
).returncode
)
for state in ("enabled", "active", "failing")
}
def getAccountServiceStates(accounts):
for account in accounts:
enabled, active, failing = getAccountServiceState(account).values()
print(f" - \033[1;37m{account:<18}\033[0;37m ", end="", flush=True)
if not enabled:
print("\033[1;33mdisabled\033[0;37m")
elif active:
print("\033[1;32mactive\033[0;37m")
elif failing:
print("\033[1;31mfailing\033[0;37m")
else:
print("\033[1;35min an unrecognised state\033[0;37m")
if len(sys.argv) > 1:
if sys.argv[1] == "--enable":
enableAccountSystemdServices(oldAccounts)
exit()
elif sys.argv[1] == "--disable":
disableAccountSystemdServices(oldAccounts)
exit()
elif sys.argv[1] == "--status":
getAccountServiceStates(oldAccounts)
exit()
2020-07-11 19:36:50 +00:00
elif sys.argv[1] == "--help":
print("""\033[1;37mMbsync to IMAP Notify config generator.\033[0;37m
Usage: mbsync-imapnotify [options]
Options:
--enable enable all services
--disable disable all services
--status fetch the status for all services
--help show this help
""", end='')
exit()
2020-05-23 04:17:08 +00:00
else:
print(f"\033[0;31mFlag {sys.argv[1]} not recognised\033[0;37m")
exit()
mbsyncData = open(mbsyncFile, "r").read()
currentAccountNumber = 0
totalAccounts = len(re.findall(r"^IMAPAccount", mbsyncData, re.M))
def main():
print("\033[1;34m:: MbSync to Go IMAP notify config file creator ::\033[0;37m")
shutil.rmtree(imapnotifyConfigFolder)
imapnotifyConfigFolder.mkdir(exist_ok=False)
print("\033[1;30mImap Notify config dir purged\033[0;37m")
print(f"Identified \033[1;32m{totalAccounts}\033[0;32m accounts.\033[0;37m")
for line in mbsyncData.split("\n"):
processLine(line)
finaliseAccount()
print(
f"\nConfig files generated for \033[1;36m{len(successfulAdditions)}\033[0;36m"
+ f" out of \033[1;36m{totalAccounts}\033[0;37m accounts.\n"
)
processSystemdServices()
if __name__ == "__main__":
main()
#+END_SRC
**** Systemd
We then have a service file to run ~goimapnotify~ on all of these generated config files.
We'll use a template service file so we can enable a unit per-account.
#+BEGIN_SRC systemd :tangle ~/.config/systemd/user/goimapnotify@.service
[Unit]
Description=IMAP notifier using IDLE, golang version.
ConditionPathExists=%h/.imapnotify/%I/notify.conf
After=network.target
[Service]
2020-06-29 19:03:37 +00:00
ExecStart=%h/.local/bin/goimapnotify -conf %h/.imapnotify/%I/notify.conf
2020-05-23 04:17:08 +00:00
Restart=always
RestartSec=30
[Install]
WantedBy=default.target
#+END_SRC
Enabling the service is actually taken care of by that python script.
From one or two small tests, this can bring the delay down to as low as five
seconds, which I'm quite happy with.
This works well for fetching new mail, but we also want to propagate other
changes (e.g. marking mail as read), and make sure we're up to date at the
start, so for that I'll do the 'normal' thing and run ~mbsync -all~ every so often
--- let's say five minutes.
We can accomplish this via a systemd timer, and service file.
#+BEGIN_SRC systemd :tangle ~/.config/systemd/user/mbsync.timer
[Unit]
Description=call mbsync on all accounts every 5 minutes
ConditionPathExists=%h/.mbsyncrc
[Timer]
OnBootSec=5m
OnUnitInactiveSec=5m
[Install]
WantedBy=default.target
#+END_SRC
#+BEGIN_SRC systemd :tangle ~/.config/systemd/user/mbsync.service
[Unit]
Description=mbsync service, sync all mail
Documentation=man:mbsync(1)
ConditionPathExists=%h/.mbsyncrc
[Service]
Type=oneshot
ExecStart=/usr/bin/mbsync -c %h/.mbsyncrc --all
2020-05-23 04:17:08 +00:00
[Install]
WantedBy=mail.target
#+END_SRC
Enabling (and starting) this is as simple as
#+BEGIN_SRC shell :tangle (if (string= "enabled\n" (shell-command-to-string "systemctl --user is-enabled mbsync.timer")) "no" "setup.sh")
2020-05-23 04:17:08 +00:00
systemctl --user enable mbsync.timer --now
#+END_SRC
*** Indexing/Searching
This is performed by [[https://www.djcbsoftware.nl/code/mu/][Mu]]. This is a tool for finding emails stored in the [[http://en.wikipedia.org/wiki/Maildir][Maildir]] format.
According to the homepage, it's main features are
+ Fast indexing
+ Good searching
+ Support for encrypted and signed messages
+ Rich CLI tooling
+ accent/case normalisation
+ strong integration with email clients
Unfortunately ~mu~ is not currently packaged from me. Oh well, I guess I'm
building it from source then. I needed to install these packages
+ =gmime-devel=
+ =xapian-core-devel=
#+NAME: Install mu from source
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "mu") "no" "setup.sh")
2020-05-23 04:17:08 +00:00
cd ~/.local/lib/
git clone https://github.com/djcb/mu.git
cd ./mu
./autogen.sh
make
sudo make install
#+END_SRC
To check how my version compares to the latest published:
#+BEGIN_SRC shell :tangle no
2020-05-23 04:17:08 +00:00
curl --silent "https://api.github.com/repos/djcb/mu/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
mu --version | head -n 1 | sed 's/.* version //'
2020-05-23 04:17:08 +00:00
#+END_SRC
#+RESULTS:
| 1.4.6 |
| 1.4.6 |
*** Sending
[[https://www.nongnu.org/smtpmail/][SmtpMail]] seems to be the 'default' starting point, but that's not packaged for
me. [[https://marlam.de/msmtp/][msmtp]] is however, so I'll give that a shot. Reading around a bit (googling
"msmtp vs sendmail" for example) almost every comparison mentioned seems to
suggest msmtp to be a better choice. I have seen the following points raised
+ ~sendmail~ has several vulnerabilities
+ ~sendmail~ is tedious to configure
+ ~ssmtp~ is no longer maintained
+ ~msmtp~ is a maintained alternative to ~ssmtp~
+ ~msmtp~ is easier to configure
The config file is [[file:~/.msmtprc][~/.msmtprc]]
**** System hackery
Unfortunately, I seem to have run into a [[https://bugs.archlinux.org/task/44994][bug]] present in my packaged version, so
we'll just install the latest from source.
For full use of the ~auth~ options, I need =GNU SASL=, which isn't packaged for me.
I don't think I want it, but in case I do, I'll need to do this.
#+NAME: Install gsasl from source
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "msmtp") "no" "setup.sh")
2020-05-23 04:17:08 +00:00
export GSASL_VERSION=1.8.1
cd ~/.local/lib/
curl "ftp://ftp.gnu.org/gnu/gsasl/libgsasl-$GSASL_VERSION.tar.gz" | tar xz
curl "ftp://ftp.gnu.org/gnu/gsasl/gsasl-$GSASL_VERSION.tar.gz" | tar xz
cd "./libgsasl-$GSASL_VERSION"
./configure
make
sudo make install
cd ..
cd "./gsasl-$VERSION"
./configure
make
sudo make install
cd ..
#+END_SRC
Now actually compile ~msmtp~.
#+NAME: Install msmtp from source
#+BEGIN_SRC shell :eval no :tangle (if (executable-find "msmtp") "no" "setup.sh")
2020-05-23 04:17:08 +00:00
cd ~/.local/lib/
git clone https://github.com/marlam/msmtp-mirror.git ./msmtp
cd ./msmtp
libtoolize --force
aclocal
autoheader
automake --force-missing --add-missing
autoconf
# if using GSASL
# PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure --with-libgsasl
./configure
make
sudo make install
#+END_SRC
If using =GSASL= (from earlier) we need to make ensure that the dynamic library in
in the library path. We can do by adding an executable with the same name
earlier on in my ~$PATH~.
#+BEGIN_SRC sh :tangle no :shebang "#!/bin/sh"
LD_LIBRARY_PATH=/usr/local/lib exec /usr/local/bin/msmtp "$@"
#+END_SRC
*** Mu4e
Webmail clients are nice and all, but I still don't believe that SPAs in my
browser can replaced desktop apps ... sorry Gmail. I'm also liking google less
and less.
Mailspring is a decent desktop client, quite lightweight for electron
(apparently the backend is in =C=, which probably helps), however I miss Emacs
stuff.
While =Notmuch= seems very promising, and I've heard good things about it, it
doesn't seem to make any changes to the emails themselves. All data is stored in
Notmuch's database. While this is a very interesting model, occasionally I need
to pull up an email on say my phone, and so not I want the tagging/folders etc.
to be applied to the mail itself --- not stored in a database.
On the other hand =Mu4e= is also talked about a lot in positive terms, and seems
to possess a similarly strong feature set --- and modifies the mail itself (I.e.
information is accessible without the database). =Mu4e= also seems to have a large
user base, which tends to correlate with better support and attention.
2020-05-23 04:17:08 +00:00
As I installed mu4e from source, I need to add the =/usr/local/= loadpath so Mu4e has a chance of loading
#+BEGIN_SRC emacs-lisp :tangle (if (file-directory-p "/usr/local/share/emacs/site-lisp/mu4e") "yes" "no")
2020-05-23 04:17:08 +00:00
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
#+END_SRC
**** Viewing Mail
There seem to be some advantages with using Gnus' article view (such as inline
images), and judging from [[https://github.com/djcb/mu/pull/1442#issuecomment-591695814][djcb/mu!1442 (comment)]] this seems to be the 'way of
the future' for mu4e.
#+BEGIN_SRC emacs-lisp
(setq mu4e-view-use-gnus t)
#+END_SRC
2020-05-23 04:17:08 +00:00
There are some all-the-icons font related issues, so we need to redefine the
fancy chars, and make sure they get the correct width.
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defun my-string-width (str)
"Return the width in pixels of a string in the current
window's default font. If the font is mono-spaced, this
will also be the width of all other printable characters."
(let ((window (selected-window))
(remapping face-remapping-alist))
(with-temp-buffer
(make-local-variable 'face-remapping-alist)
(setq face-remapping-alist remapping)
(set-window-buffer window (current-buffer))
(insert str)
(car (window-text-pixel-size)))))
(cl-defun mu4e~normalised-icon (name &key set colour height v-adjust)
"Convert :icon declaration to icon"
(let* ((icon-set (intern (concat "all-the-icons-" (or set "faicon"))))
(v-adjust (or v-adjust 0.02))
(height (or height 0.8))
(icon (if colour
(apply icon-set `(,name :face ,(intern (concat "all-the-icons-" colour)) :height ,height :v-adjust ,v-adjust))
(apply icon-set `(,name :height ,height :v-adjust ,v-adjust))))
(icon-width (my-string-width icon))
(space-width (my-string-width " "))
(space-factor (- 2 (/ (float icon-width) space-width))))
(concat (propertize " " 'display `(space . (:width ,space-factor))) icon)
))
(defun mu4e~initialise-icons ()
2020-05-23 04:17:08 +00:00
(setq mu4e-use-fancy-chars t
mu4e-headers-draft-mark (cons "D" (mu4e~normalised-icon "pencil"))
mu4e-headers-flagged-mark (cons "F" (mu4e~normalised-icon "flag"))
mu4e-headers-new-mark (cons "N" (mu4e~normalised-icon "sync" :set "material" :height 0.8 :v-adjust -0.10))
mu4e-headers-passed-mark (cons "P" (mu4e~normalised-icon "arrow-right"))
mu4e-headers-replied-mark (cons "R" (mu4e~normalised-icon "arrow-right"))
mu4e-headers-seen-mark (cons "S" "") ;(mu4e~normalised-icon "eye" :height 0.6 :v-adjust 0.07 :colour "dsilver"))
mu4e-headers-trashed-mark (cons "T" (mu4e~normalised-icon "trash"))
mu4e-headers-attach-mark (cons "a" (mu4e~normalised-icon "file-text-o" :colour "silver"))
mu4e-headers-encrypted-mark (cons "x" (mu4e~normalised-icon "lock"))
mu4e-headers-signed-mark (cons "s" (mu4e~normalised-icon "certificate" :height 0.7 :colour "dpurple"))
mu4e-headers-unread-mark (cons "u" (mu4e~normalised-icon "eye-slash" :v-adjust 0.05))))
(if (display-graphic-p)
(mu4e~initialise-icons)
;; When it's the server, wait till the first graphical frame
(add-hook! 'server-after-make-frame-hook
(defun mu4e~initialise-icons-hook ()
(when (display-graphic-p)
(mu4e~initialise-icons)
(remove-hook #'mu4e~initialise-icons-hook))))))
2020-05-23 04:17:08 +00:00
#+END_SRC
2020-05-23 10:54:09 +00:00
To account for the increase width of each flag character, and make perform a
few more visual tweaks, we'll tweak the headers a bit
2020-05-23 04:17:08 +00:00
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defun mu4e-header-colourise (str)
(let* ((str-sum (apply #'+ (mapcar (lambda (c) (% c 3)) str)))
(colour (nth (% str-sum (length mu4e-header-colourised-faces))
mu4e-header-colourised-faces)))
(put-text-property 0 (length str) 'face colour str)
str))
(defvar mu4e-header-colourised-faces
'(all-the-icons-lblue
all-the-icons-purple
all-the-icons-blue-alt
all-the-icons-green
all-the-icons-maroon
all-the-icons-yellow
all-the-icons-orange))
2020-05-23 04:17:08 +00:00
(setq mu4e-headers-fields
2020-05-23 10:54:09 +00:00
'((:account . 12)
2020-05-23 04:17:08 +00:00
(:human-date . 8)
(:flags . 6)
(:from . 25)
(:folder . 10)
2020-05-23 10:54:09 +00:00
(:recipnum . 2)
(:subject))
mu4e-headers-date-format "%d/%m/%y"
mu4e-headers-time-format "%T")
2020-05-23 04:17:08 +00:00
(plist-put (cdr (assoc :flags mu4e-header-info)) :shortname " Flags") ; default=Flgs
2020-05-23 10:54:09 +00:00
(setq mu4e-header-info-custom
'((:account .
(:name "Account" :shortname "Account" :help "Which account this email belongs to" :function
(lambda (msg)
(let ((maildir
(mu4e-message-field msg :maildir)))
(mu4e-header-colourise (replace-regexp-in-string "^gmail" (propertize "g" 'face 'bold-italic)
(format "%s"
(substring maildir 1
(string-match-p "/" maildir 1)))))))))
(:folder .
(:name "Folder" :shortname "Folder" :help "Lowest level folder" :function
(lambda (msg)
(let ((maildir
(mu4e-message-field msg :maildir)))
(mu4e-header-colourise (replace-regexp-in-string "\\`.*/" "" maildir))))))
2020-05-23 10:54:09 +00:00
(:recipnum .
(:name "Number of recipients"
:shortname " ⭷"
:help "Number of recipients for this message"
:function
(lambda (msg)
(propertize (format "%2d"
(+ (length (mu4e-message-field msg :to))
(length (mu4e-message-field msg :cc))))
'face 'mu4e-footer-face)))))))
2020-05-23 04:17:08 +00:00
#+END_SRC
While considering width and the mu4e header view --- it's hard to see enough with
2020-07-19 16:33:28 +00:00
a with less than 120 characters, so let's add a hook to mu4e's header mode.
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defvar mu4e-min-header-frame-width 120
"Minimum reasonable with for the header view.")
(defun mu4e-widen-frame-maybe ()
"Expand the frame with if it's less than `mu4e-min-header-frame-width'."
(when (< (frame-width) mu4e-min-header-frame-width)
(set-frame-width (selected-frame) mu4e-min-header-frame-width)))
(add-hook 'mu4e-headers-mode-hook #'mu4e-widen-frame-maybe))
#+END_SRC
Due to evil, none of the marking commands work when making a visual selection in
the headers view of mu4e. Without overriding any evil commands we may actually
want to use in and evil selection, this can be easily fixed.
#+BEGIN_SRC emacs-lisp
(map! :map mu4e-headers-mode-map
:after mu4e
:v "*" #'mu4e-headers-mark-for-something
:v "!" #'mu4e-headers-mark-for-read
:v "?" #'mu4e-headers-mark-for-unread
:v "u" #'mu4e-headers-mark-for-unmark)
#+END_SRC
2020-07-19 16:33:28 +00:00
The main mu4e window is ... alright. I'm not afraid of Unicode though, so I'll
2020-05-23 04:17:08 +00:00
define a fancier version. Look, it's the asterisks. We can do better than
asterisks. The keybindings can also be made nicer, why have ~[x]~ when we can just
have a bold, coloured ~x~. Does the same job, while looking much less garish.
We don't put this in an ~(after! ...)~ block as =evil-collection-mu4e= calls
~mu4e~main-action-str~ in Doom's mu4e ~(usepackage! ...)~.
#+BEGIN_SRC emacs-lisp
(defadvice! mu4e~main-action-prettier-str (str &optional func-or-shortcut)
"Highlight the first occurrence of [.] in STR.
If FUNC-OR-SHORTCUT is non-nil and if it is a function, call it
when STR is clicked (using RET or mouse-2); if FUNC-OR-SHORTCUT is
a string, execute the corresponding keyboard action when it is
clicked."
:override #'mu4e~main-action-str
(let ((newstr
(replace-regexp-in-string
"\\[\\(..?\\)\\]"
(lambda(m)
(format "%s"
(propertize (match-string 1 m) 'face '(mode-line-emphasis bold))))
(replace-regexp-in-string "\t\\*" "\t⚫" str)))
(map (make-sparse-keymap))
(func (if (functionp func-or-shortcut)
func-or-shortcut
(if (stringp func-or-shortcut)
(lambda()(interactive)
(execute-kbd-macro func-or-shortcut))))))
(define-key map [mouse-2] func)
(define-key map (kbd "RET") func)
(put-text-property 0 (length newstr) 'keymap map newstr)
(put-text-property (string-match "[A-Za-z].+$" newstr)
(- (length newstr) 1) 'mouse-face 'highlight newstr)
newstr))
(setq evil-collection-mu4e-end-region-misc "quit")
#+END_SRC
I'd also quite like an easy way to be able to move away from the created mu4e
workspace without closing the process (which rules "quit" out). The obvious
analogous idea is "hide", and it turns out that the =h= key is conveniently
unbound in the main mu4e view.
#+BEGIN_SRC emacs-lisp
(map! :map mu4e-main-mode-map
:after mu4e
:nive "h" #'+workspace/other)
#+END_SRC
I often find myself viewing new mails using the Emacs client. When opening a new
window and switching to view the mu4e workspace, and empty workspace is left. We
2020-07-19 16:33:28 +00:00
can try to avoid this workspace pollution by deleting the current workspace
before switching if it is empty.
#+BEGIN_SRC emacs-lisp
(defadvice! delete-current-worspace-if-empty ()
"Close the current workspace if it is empty."
:before #'=mu4e
(unless (+workspace-buffer-list)
(+workspace-delete (+workspace-current-name))))
#+END_SRC
2020-05-23 04:17:08 +00:00
**** Sending Mail
Let's send emails too.
#+BEGIN_SRC emacs-lisp
(after! mu4e
2020-07-19 16:10:59 +00:00
(setq sendmail-program "/usr/bin/msmtp"
2020-05-23 04:17:08 +00:00
send-mail-function 'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from"); , "--read-recipients")
message-send-mail-function 'message-send-mail-with-sendmail))
#+END_SRC
2020-07-19 16:33:28 +00:00
It's also nice to avoid accidentally sending emails with the wrong account.
If we can send from the address in the ~To~ field, let's do that. Opening an ~ivy~
prompt otherwise also seems sensible.
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defun my-mu4e-set-account ()
"Set the account for composing a message."
(unless (and mu4e-compose-parent-message
(let ((to (cdr (car (mu4e-message-field mu4e-compose-parent-message :to))))
(from (cdr (car (mu4e-message-field mu4e-compose-parent-message :from)))))
(if (member to (plist-get mu4e~server-props :personal-addresses))
(setq user-mail-address to)
(if (member from (plist-get mu4e~server-props :personal-addresses))
(setq user-mail-address from)
nil))))
(ivy-read "Account: " (plist-get mu4e~server-props :personal-addresses) :action (lambda (candidate) (setq user-mail-address candidate)))))
(add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account))
#+END_SRC
We can register Emacs as a potential email client with the following desktop
file, thanks to Etienne Deparis's [[https://etienne.depar.is/emacs.d/mu4e.html][Mu4e customization]].
#+BEGIN_SRC conf :tangle ~/.local/share/applications/emacsmail.desktop :mkdirp yes
[Desktop Entry]
Name=Compose message in Emacs
GenericName=Compose a new message with Mu4e in Emacs
Comment=Open mu4e compose window
MimeType=x-scheme-handler/mailto;
Exec=emacsclient -create-frame --alternate-editor="" --no-wait --eval '(progn (x-focus-frame nil) (mu4e-compose-from-mailto "%u"))'
Icon=emacs
Type=Application
Terminal=false
Categories=Network;Email;
StartupWMClass=Emacs
#+END_SRC
To register this, just call
#+BEGIN_SRC shell
update-desktop-database ~/.local/share/applications
#+END_SRC
2020-05-23 04:17:08 +00:00
We also want to define ~mu4e-compose-from-mailto~.
#+BEGIN_SRC emacs-lisp
(defun mu4e-compose-from-mailto (mailto-string)
(require 'mu4e)
(unless mu4e~server-props (mu4e t) (sleep-for 0.1))
(let* ((mailto (rfc2368-parse-mailto-url mailto-string))
(to (cdr (assoc "To" mailto)))
(subject (or (cdr (assoc "Subject" mailto)) ""))
(body (cdr (assoc "Body" mailto)))
(org-msg-greeting-fmt (if (assoc "Body" mailto)
(replace-regexp-in-string "%" "%%"
(cdr (assoc "Body" mailto)))
org-msg-greeting-fmt))
(headers (-filter (lambda (spec) (not (-contains-p '("To" "Subject" "Body") (car spec)))) mailto)))
(mu4e~compose-mail to subject headers)))
#+END_SRC
This may not quite function as intended for now due to [[github:jeremy-compostella/org-msg/issues/52][jeremy-compostella/org-msg#52]].
2020-05-24 03:38:36 +00:00
**** Getting notified
For this we can use =mu4e-alert=. We can also perform a few tweaks to make it a
bit more visually appealing.
#+BEGIN_SRC emacs-lisp :noweb yes
2020-05-24 03:38:36 +00:00
(use-package! mu4e-alert
:after mu4e
:config
(mu4e-alert-enable-mode-line-display)
2020-05-25 17:16:23 +00:00
(mu4e-alert-enable-notifications)
(mu4e-alert-set-default-style 'libnotify)
(setq mu4e-alert-icon "/usr/share/icons/Papirus/64x64/apps/evolution.svg")
2020-05-25 17:16:23 +00:00
(defun mu4e-alert-iconised-modeline-formatter (mail-count)
"Formatter used to get the string to be displayed in the mode-line, using all-the-icons.
MAIL-COUNT is the count of mails for which the string is to displayed"
(when (not (zerop mail-count))
(concat " "
(propertize
(concat
(all-the-icons-material "mail_outline")
(if (zerop mail-count)
""
(format " %d" mail-count)))
'help-echo (concat (if (= mail-count 1)
"You have an unread email"
(format "You have %s unread emails" mail-count))
"\nClick here to view "
(if (= mail-count 1) "it" "them"))
'mouse-face 'mode-line-highlight
'keymap '(mode-line keymap
(mouse-1 . mu4e-alert-view-unread-mails)
(mouse-2 . mu4e-alert-view-unread-mails)
(mouse-3 . mu4e-alert-view-unread-mails))))))
2020-05-25 17:16:23 +00:00
(setq mu4e-alert-modeline-formatter #'mu4e-alert-iconised-modeline-formatter)
(setq mu4e-alert-email-notification-types '(subjects))
(defun mu4e-alert-grouped-mail-notification-formatter-with-bell (mail-group all-mails)
"Default function to format MAIL-GROUP for notification.
ALL-MAILS are the all the unread emails"
(shell-command "paplay /usr/share/sounds/freedesktop/stereo/message.oga")
(if (> (length mail-group) 1)
(let* ((mail-count (length mail-group))
(total-mails (length all-mails))
(first-mail (car mail-group))
(title-prefix (format "You have %d unread emails"
mail-count))
(field-value (mu4e-alert--get-group first-mail))
(title-suffix (format (pcase mu4e-alert-group-by
(`:from "from %s:")
(`:to "to %s:")
(`:maildir "in %s:")
(`:priority "with %s priority:")
(`:flags "with %s flags:"))
field-value))
(title (format "%s %s" title-prefix title-suffix)))
(list :title title
:body (s-join "\n"
(mapcar (lambda (mail)
(format "%s<b>%s</b> • %s"
(cond
((plist-get mail :in-reply-to) "⮩ ")
((string-match-p "\\`Fwd:"
(plist-get mail :subject)) " ⮯ ")
(t "  "))
(truncate-string-to-width (caar (plist-get mail :from))
20 nil nil t)
(truncate-string-to-width
(replace-regexp-in-string "\\`Re: \\|\\`Fwd: " ""
(plist-get mail :subject))
40 nil nil t)))
mail-group))))
(let* ((new-mail (car mail-group))
(subject (plist-get new-mail :subject))
(sender (caar (plist-get new-mail :from))))
(list :title sender :body subject))))
(setq mu4e-alert-grouped-mail-notification-formatter #'mu4e-alert-grouped-mail-notification-formatter-with-bell))
#+END_SRC
**** Process control
Here's what I want
+ An instance of Mu4e to be active all the time, for notifications
+ No crufty timers on loops et. al
I think a file watch etc. setup similar to that in [[*Rebuild mail index while using mu4e][Rebuild mail index while
using mu4e]] is probably the cleanest way to operate. We can put the current PID
2020-07-19 16:33:28 +00:00
in the file and check for exitance too.
#+BEGIN_SRC emacs-lisp
(after! mu4e
(defvar mu4e-lock-file "/tmp/mu4e_lock"
"Location of the lock file which stores the PID of the process currenty running mu4e")
(defvar mu4e-lock-request-file "/tmp/mu4e_lock_request"
"Location of the lock file for which creating indicated that another process wants the lock to be released")
(defvar mu4e-lock-greedy nil
"Whether to 'grab' the `mu4e-lock-file' if nobody else has it, i.e. start Mu4e")
(defvar mu4e-lock-relaxed nil
"Whether if someone else wants the lock (signaled via `mu4e-lock-request-file'), we should stop Mu4e and let go of it")
(defun mu4e-lock-pid-info ()
"Get info on the PID refered to in `mu4e-lock-file' in the form (pid . process-attributes)
If the file or process do not exist, the lock file is deleted an nil returned."
(when (file-exists-p mu4e-lock-file)
(let* ((pid (string-to-number (f-read-text mu4e-lock-file 'utf-8)))
(process (process-attributes pid)))
(if process (cons pid process)
(delete-file mu4e-lock-file) nil))))
(defun mu4e-lock-avalible (&optional strict)
"If the `mu4e-lock-file' is avalible (unset or owned by this emacs) return t.
If STRICT only accept an unset lock file."
(not (when-let* ((lock-info (mu4e-lock-pid-info))
(pid (car lock-info)))
(when (or strict (/= (emacs-pid) pid)) t))))
(defadvice! mu4e-lock-file-delete-maybe ()
"Check `mu4e-lock-file', and delete it if this process is responsible for it."
:after #'mu4e-quit
(when (mu4e-lock-avalible)
(delete-file mu4e-lock-file)
(file-notify-rm-watch mu4e-lock--request-watcher)))
(add-hook 'kill-emacs-hook #'mu4e-lock-file-delete-maybe)
(defadvice! mu4e-lock-start (orig-fun &optional callback)
"Check `mu4e-lock-file', and if another process is responsible for it, abort starting.
Else, write to this process' PID to the lock file"
:around #'mu4e~start
(unless (mu4e-lock-avalible)
(shell-command (format "touch %s" mu4e-lock-request-file))
(message "Lock file exists, requesting that it be given up")
(sleep-for 0.1)
(delete-file mu4e-lock-request-file))
(if (not (mu4e-lock-avalible))
(user-error "Unfortunately another Emacs is already doing stuff with Mu4e, and you can only have one at a time")
(f-write-text (number-to-string (emacs-pid)) 'utf-8 mu4e-lock-file)
(delete-file mu4e-lock-request-file)
(funcall orig-fun callback)
(setq mu4e-lock--request-watcher
(file-notify-add-watch mu4e-lock-request-file
'(change)
#'mu4e-lock-request))))
(defvar mu4e-lock--file-watcher nil)
(defvar mu4e-lock--file-just-deleted nil)
(defvar mu4e-lock--request-watcher nil)
(defun mu4e-lock-add-watcher ()
(setq mu4e-lock--file-just-deleted nil)
(file-notify-rm-watch mu4e-lock--file-watcher)
(setq mu4e-lock--file-watcher
(file-notify-add-watch mu4e-lock-file
'(change)
#'mu4e-lock-file-updated)))
(defun mu4e-lock-request (event)
"Handle another process requesting the Mu4e lock."
(when (equal (nth 1 event) 'created)
(when mu4e-lock-relaxed
(mu4e~stop)
(file-notify-rm-watch mu4e-lock--file-watcher)
(message "Someone else wants to use Mu4e, releasing lock")
(delete-file mu4e-lock-file)
(run-at-time 0.2 nil #'mu4e-lock-add-watcher))
(delete-file mu4e-lock-request-file)))
(defun mu4e-lock-file-updated (event)
(if mu4e-lock--file-just-deleted
(mu4e-lock-add-watcher)
(when (equal (nth 1 event) 'deleted)
(setq mu4e-lock--file-just-deleted t)
(when (and mu4e-lock-greedy (mu4e-lock-avalible t))
(message "Noticed Mu4e lock was avalible, grabbed it")
(run-at-time 0.2 nil #'mu4e~start))
))))
2020-05-24 03:38:36 +00:00
#+END_SRC
2020-05-23 04:17:08 +00:00
*** Org Msg
2020-05-23 19:17:49 +00:00
#+BEGIN_SRC emacs-lisp :noweb yes
(defvar org-msg-currently-exporting nil
"Helper variable to indicate whether org-msg is currently exporting the org buffer to HTML.
Usefull for affecting some of my HTML export config.")
2020-05-23 04:17:08 +00:00
(use-package! org-msg
:after mu4e
:config
(setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t tex:dvipng"
org-msg-startup "hidestars indent inlineimages"
org-msg-greeting-fmt "\nHi %s,\n\n"
org-msg-greeting-name-limit 3
2020-05-23 19:17:49 +00:00
org-msg-text-plain-alternative t)
(map! :map org-msg-edit-mode-map
:n "G" #'org-msg-goto-body)
(defadvice! org-msg--now-exporting (&rest _)
:before #'org-msg-org-to-xml
(setq org-msg-currently-exporting t))
(defadvice! org-msg--not-exporting (&rest _)
:after #'org-msg-org-to-xml
(setq org-msg-currently-exporting nil))
<<org-msg-restyle>>
(org-msg-mode t))
#+END_SRC
To make this look a little nicer, we will overwrite the default style.
#+NAME: org-msg-restyle
#+BEGIN_SRC emacs-lisp :tangle no
(setq org-msg-enforce-css
(let* ((font-family '(font-family . "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell,\
\"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";"))
(monospace-font '(font-family . "SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;"))
(font-size '(font-size . "11pt"))
(font `(,font-family ,font-size))
(line-height '(line-height . "1.2"))
(theme-color "#2654BF")
(bold '(font-weight . "bold"))
(color `(color . ,theme-color))
(table `((margin-top . "6px") (margin-bottom . "6px")
(border-left . "none") (border-right . "none")
(border-top . "2px solid #222222") (border-bottom . "2px solid #222222")
))
(ftl-number `(,color ,bold (text-align . "left")))
2020-05-23 19:17:49 +00:00
(inline-modes '(asl c c++ conf cpp csv diff ditaa emacs-lisp
fundamental ini json makefile man org plantuml
python sh xml))
(inline-src `((background-color . "rgba(27,31,35,.05)")
(border-radius . "3px")
(padding . ".2em .4em")
(font-size . "90%") ,monospace-font
(margin . 0)))
2020-05-23 19:17:49 +00:00
(code-src
(mapcar (lambda (mode)
`(code ,(intern (concat "src src-" (symbol-name mode)))
,inline-src))
inline-modes)))
`((del nil ((color . "grey") (border-left . "none")
(text-decoration . "line-through") (margin-bottom . "0px")
(margin-top . "10px") (line-height . "11pt")))
2020-05-23 19:17:49 +00:00
(a nil (,color))
(a reply-header ((color . "black") (text-decoration . "none")))
(div reply-header ((padding . "3.0pt 0in 0in 0in")
(border-top . "solid #e1e1e1 1.0pt")
(margin-bottom . "20px")))
(span underline ((text-decoration . "underline")))
(li nil (,line-height (margin-bottom . "0px")
(margin-top . "2px")))
2020-05-23 19:17:49 +00:00
(nil org-ul ((list-style-type . "square")))
(nil org-ol (,@font ,line-height (margin-bottom . "0px")
(margin-top . "0px") (margin-left . "30px")
(padding-top . "0px") (padding-left . "5px")))
(nil signature (,@font (margin-bottom . "20px")))
(blockquote nil ((padding . "0px 10px") (margin-left . "10px")
(margin-top . "20px") (margin-bottom . "0")
(border-left . "3px solid #ccc") (font-style . "italic")
(background . "#f9f9f9")))
(code nil (,font-size ,monospace-font (background . "#f9f9f9")))
,@code-src
(nil linenr ((padding-right . "1em")
(color . "black")
(background-color . "#aaaaaa")))
(pre nil ((line-height . "1.2")
(color . ,(doom-color 'fg))
(background-color . ,(doom-color 'bg))
(margin . "4px 0px 8px 0px")
(padding . "8px 12px")
(width . "95%")
(border-radius . "5px")
(font-weight . "500")
,monospace-font))
(div org-src-container ((margin-top . "10px")))
(nil figure-number ,ftl-number)
(nil table-number)
(caption nil ((text-align . "left")
(background . ,theme-color)
(color . "white")
,bold))
(nil t-above ((caption-side . "top")))
(nil t-bottom ((caption-side . "bottom")))
(nil listing-number ,ftl-number)
(nil figure ,ftl-number)
(nil org-src-name ,ftl-number)
(img nil ((vertical-align . "middle")
(max-width . "100%")))
(img latex-fragment-inline ((transform . ,(format "translateY(-1px) scale(%.3f)"
2020-05-23 19:17:49 +00:00
(/ 1.0 (if (boundp 'preview-scale)
preview-scale 1.4))))
(margin . "0 -0.35em")))
(table nil (,@table ,line-height (border-collapse . "collapse")))
(th nil ((border . "none") (border-bottom . "1px solid #222222")
(background-color . "#EDEDED") (font-weight . "500")
(padding . "3px 10px")))
(td nil (,@table (padding . "1px 10px")
(background-color . "#f9f9f9") (border . "none")))
(td org-left ((text-align . "left")))
(td org-right ((text-align . "right")))
(td org-center ((text-align . "center")))
(kbd nil ((border . "1px solid #d1d5da") (border-radius . "3px")
(box-shadow . "inset 0 -1px 0 #d1d5da") (background-color . "#fafbfc")
(color . "#444d56") (padding . "3px 5px") (display . "inline-block")))
(div outline-text-4 ((margin-left . "15px")))
(div outline-4 ((margin-left . "10px")))
(h4 nil ((margin-bottom . "0px") (font-size . "11pt")))
2020-05-23 19:17:49 +00:00
(h3 nil ((margin-bottom . "0px")
,color (font-size . "14pt")))
2020-05-23 19:17:49 +00:00
(h2 nil ((margin-top . "20px") (margin-bottom . "20px")
,color (font-size . "18pt")))
2020-05-23 19:17:49 +00:00
(h1 nil ((margin-top . "20px")
(margin-bottom . "0px") ,color (font-size . "24pt")))
2020-05-23 19:17:49 +00:00
(p nil ((text-decoration . "none") (margin-bottom . "0px")
(margin-top . "10px") (line-height . "11pt") ,font-size
(max-width . "100ch")))
2020-05-23 19:17:49 +00:00
(b nil ((font-weight . "500") (color . ,theme-color)))
(div nil (,@font (line-height . "12pt"))))))
2020-05-23 04:17:08 +00:00
#+END_SRC
** Org Chef
Loading after org seems a bit premature. Let's just load it when we try to use
it, either by command or in a capture template.
#+BEGIN_SRC emacs-lisp
(use-package! org-chef
:commands (org-chef-insert-recipe org-chef-get-recipe-from-url))
#+END_SRC
2020-04-29 08:47:48 +00:00
** Projectile
Looking at documentation via =SPC h f= and =SPC h v= and looking at the source can
add package src directories to projectile. This isn't desirable in my opinion.
#+BEGIN_SRC emacs-lisp
(setq projectile-ignored-projects '("~/" "/tmp" "~/.emacs.d/.local/straight/repos/"))
(defun projectile-ignored-project-function (filepath)
"Return t if FILEPATH is within any of `projectile-ignored-projects'"
(or (mapcar (lambda (p) (s-starts-with-p p filepath)) projectile-ignored-projects)))
#+END_SRC
** Lexic
We start off my loading =lexic=, then we'll integrate it into pre-existing
definition functionality (like ~+lookup/dictionary-definition~).
#+BEGIN_SRC emacs-lisp
(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 "C-p" #'lexic-search-history-backwards
:n "C-n" #'lexic-search-history-forwards
:n "/" (cmd! (call-interactively #'lexic-search))))
#+END_SRC
Now let's use this instead of wordnet.
#+BEGIN_SRC emacs-lisp
(defadvice! +lookup/dictionary-definition-lexic (identifier &optional arg)
"Look up the definition of the word at point (or selection) using `lexic-search'."
:override #'+lookup/dictionary-definition
(interactive
(list (or (doom-thing-at-point-or-region 'word)
(read-string "Look up in dictionary: "))
current-prefix-arg))
(lexic-search identifier nil nil t))
#+END_SRC
2020-04-29 08:47:48 +00:00
** Smart Parentheses
#+BEGIN_SRC emacs-lisp
(sp-local-pair
'(org-mode)
"<<" ">>"
:actions '(insert))
#+END_SRC
** Spray
Let's make this suit me slightly better.
#+BEGIN_SRC emacs-lisp
(setq spray-wpm 500
spray-height 700)
#+END_SRC
** Theme magic
2020-09-02 01:53:25 +00:00
Let's automatically update terminals on theme change (as long as ~pywal~ is available).
#+BEGIN_SRC emacs-lisp :tangle (if (executable-find "wal") "yes" "no")
2020-04-29 08:47:48 +00:00
(add-hook 'doom-load-theme-hook 'theme-magic-from-emacs)
#+END_SRC
** Tramp
Let's try to make tramp handle prompts better
#+BEGIN_SRC emacs-lisp
(after! tramp
(setenv "SHELL" "/bin/bash")
2020-06-02 05:06:47 +00:00
(setq tramp-shell-prompt-pattern "\\(?:^\\| \\)[^]#$%>\n]*#?[]#$%>] *\\(\\[[0-9;]*[a-zA-Z] *\\)*")) ;; default + 
#+END_SRC
*** Troubleshooting
In case the remote shell is misbehaving, here are some things to try
**** Zsh
There are some escape code you don't want, let's make it behave more considerately.
#+BEGIN_SRC shell :eval no :tangle no
if [[ "$TERM" == "dumb" ]]; then
unset zle_bracketed_paste
unset zle
PS1='$ '
return
fi
#+END_SRC
2020-03-17 17:12:17 +00:00
** Treemacs
Quite often there are superfluous files I'm not that interested in. There's no
2020-03-23 03:00:23 +00:00
good reason for them to take up space. Let's add a mechanism to ignore them.
#+BEGIN_SRC emacs-lisp
(after! treemacs
(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)))
(if (equal treemacs-file-ignore-globs '()) nil (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))
#+END_SRC
Now, we just identify the files in question.
2020-03-17 17:12:17 +00:00
#+BEGIN_SRC emacs-lisp
2020-03-18 16:48:47 +00:00
(setq treemacs-file-ignore-extensions '(;; LaTeX
"aux"
"ptc"
"fdb_latexmk"
"fls"
"synctex.gz"
"toc"
;; LaTeX - glossary
"glg"
"glo"
"gls"
"glsdefs"
"ist"
"acn"
"acr"
"alg"
;; LaTeX - pgfplots
"mw"
;; LaTeX - pdfx
"pdfa.xmpi"
))
(setq treemacs-file-ignore-globs '(;; LaTeX
"*/_minted-*"
;; AucTeX
"*/.auctex-auto"
"*/_region_.log"
2020-03-23 03:00:23 +00:00
"*/_region_.tex"))
#+END_SRC
2020-04-29 08:47:48 +00:00
** VTerm
On my system, I want to use the ~vterm~ lib in =/usr/bin/libvterm.so=. This is not
default behaviour. Without this I see the following error message.
#+BEGIN_SRC
make[2]: *** No rule to make target 'libvterm-prefix/src/libvterm/.libs/libvterm.a', needed by '../vterm-module.so'. Stop.
make[1]: *** [CMakeFiles/Makefile2:106: CMakeFiles/vterm-module.dir/all] Error 2
make: *** [Makefile:84: all] Error 2
2020-04-09 17:15:17 +00:00
#+END_SRC
2020-04-29 08:47:48 +00:00
I asked about this in a [[https://github.com/akermu/emacs-libvterm/issues/238#issuecomment-586385773][GitHub Issue]], and the fix is to use the ~cmake~ flag
~-DUSE_SYSTEM_LIBVTERM=yes~. This can be set for when Doom recompiles ~vterm~ on
2020-04-29 08:47:48 +00:00
updating, which saves me manually compiling with the flag.
2020-04-13 19:20:37 +00:00
#+BEGIN_SRC emacs-lisp
2020-04-29 08:47:48 +00:00
(setq vterm-module-cmake-args "-DUSE_SYSTEM_LIBVTERM=yes")
2020-04-13 19:20:37 +00:00
#+END_SRC
I also use a 'fancy powerline setup' with [[https://github.com/romkatv/powerlevel10k][Powerlevel10k]], so I need to use a
patched font for my terminal. Unfortunately I haven't quite figured out how to
do this yet.
2020-04-29 08:47:48 +00:00
** Which-key
2020-04-14 09:12:09 +00:00
Let's make this popup a bit faster
#+BEGIN_SRC emacs-lisp
(setq which-key-idle-delay 0.5) ;; I need the help, I really do
#+END_SRC
2020-04-23 02:19:31 +00:00
I also think that having =evil-= appear in so many popups is a bit too verbose, let's change that, and do a few other similar tweaks while we're at it.
2020-04-17 12:34:25 +00:00
#+BEGIN_SRC emacs-lisp
(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"))
))
#+END_SRC
2020-05-16 07:44:01 +00:00
** xkcd
2020-07-19 16:33:28 +00:00
We want to set this up so it loads nicely in [[*Extra links][Extra links]].
2020-05-16 07:44:01 +00:00
#+BEGIN_SRC emacs-lisp
(use-package! xkcd
:commands (xkcd-get-json xkcd-download xkcd-get
;; now for funcs from my extension of this pkg
+xkcd-find-and-copy +xkcd-find-and-view
+xkcd-fetch-info +xkcd-select)
:config
(add-to-list 'evil-snipe-disabled-modes 'xkcd-mode)
:general (:states 'normal
:keymaps 'xkcd-mode-map
"<right>" #'xkcd-next
"n" #'xkcd-next ; evil-ish
"<left>" #'xkcd-prev
"N" #'xkcd-prev ; evil-ish
"r" #'xkcd-rand
"a" #'xkcd-rand ; because image-rotate can interfere
"t" #'xkcd-alt-text
"q" #'xkcd-kill-buffer
"o" #'xkcd-open-browser
"e" #'xkcd-open-explanation-browser
;; extras
"s" #'+xkcd-find-and-view
"/" #'+xkcd-find-and-view
"y" #'+xkcd-copy))
#+END_SRC
Let's also extend the functionality a whole bunch.
#+BEGIN_SRC emacs-lisp
(after! xkcd
(require 'emacsql-sqlite)
(defun +xkcd-select ()
"Prompt the user for an xkcd using `ivy-read' and `+xkcd-select-format'. Return the xkcd number or nil"
(let* (prompt-lines
(-dummy (maphash (lambda (key xkcd-info)
(push (+xkcd-select-format xkcd-info) prompt-lines))
+xkcd-stored-info))
(num (ivy-read (format "xkcd (%s): " xkcd-latest) prompt-lines)))
(if (equal "" num) xkcd-latest
(string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num)))))
(defun +xkcd-select-format (xkcd-info)
"Creates each ivy-read line from an xkcd info plist. Must start with the xkcd number"
(format "%-4s %-30s %s"
(propertize (number-to-string (plist-get xkcd-info :num))
'face 'counsel-key-binding)
(plist-get xkcd-info :title)
(propertize (plist-get xkcd-info :alt)
'face '(variable-pitch font-lock-comment-face))))
(defun +xkcd-fetch-info (&optional num)
"Fetch the parsed json info for comic NUM. Fetches latest when omitted or 0"
(require 'xkcd)
(when (or (not num) (= num 0))
(+xkcd-check-latest)
(setq num xkcd-latest))
(let ((res (or (gethash num +xkcd-stored-info)
(puthash num (+xkcd-db-read num) +xkcd-stored-info))))
(unless res
(+xkcd-db-write
(let* ((url (format "https://xkcd.com/%d/info.0.json" num))
(json-assoc
(if (gethash num +xkcd-stored-info)
(gethash num +xkcd-stored-info)
(json-read-from-string (xkcd-get-json url num)))))
json-assoc))
(setq res (+xkcd-db-read num)))
res))
;; since we've done this, we may as well go one little step further
(defun +xkcd-find-and-copy ()
"Prompt for an xkcd using `+xkcd-select' and copy url to clipboard"
(interactive)
(+xkcd-copy (+xkcd-select)))
(defun +xkcd-copy (&optional num)
"Copy a url to xkcd NUM to the clipboard"
(interactive "i")
(let ((num (or num xkcd-cur)))
(gui-select-text (format "https://xkcd.com/%d" num))
(message "xkcd.com/%d copied to clipboard" num)))
(defun +xkcd-find-and-view ()
"Prompt for an xkcd using `+xkcd-select' and view it"
(interactive)
(xkcd-get (+xkcd-select))
(switch-to-buffer "*xkcd*"))
(defvar +xkcd-latest-max-age (* 60 60) ; 1 hour
"Time after which xkcd-latest should be refreshed, in seconds")
;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd
(add-transient-hook! '+xkcd-select
(require 'xkcd)
(+xkcd-fetch-info xkcd-latest)
(setq +xkcd-stored-info (+xkcd-db-read-all)))
(add-transient-hook! '+xkcd-fetch-info
(xkcd-update-latest))
(defun +xkcd-check-latest ()
"Use value in `xkcd-cache-latest' as long as it isn't older thabn `+xkcd-latest-max-age'"
(unless (and (file-exists-p xkcd-cache-latest)
(< (- (time-to-seconds (current-time))
(time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest))))
+xkcd-latest-max-age))
(let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0))
(json-assoc (json-read-from-string out))
(latest (cdr (assoc 'num json-assoc))))
(when (/= xkcd-latest latest)
(+xkcd-db-write json-assoc)
(with-current-buffer (find-file xkcd-cache-latest)
(setq xkcd-latest latest)
(erase-buffer)
(insert (number-to-string latest))
(save-buffer)
(kill-buffer (current-buffer)))))
(shell-command (format "touch %s" xkcd-cache-latest))))
(defvar +xkcd-stored-info (make-hash-table :test 'eql)
"Basic info on downloaded xkcds, in the form of a hashtable")
(defadvice! xkcd-get-json--and-cache (url &optional num)
"Fetch the Json coming from URL.
If the file NUM.json exists, use it instead.
If NUM is 0, always download from URL.
The return value is a string."
:override #'xkcd-get-json
(let* ((file (format "%s%d.json" xkcd-cache-dir num))
(cached (and (file-exists-p file) (not (eq num 0))))
(out (with-current-buffer (if cached
(find-file file)
(url-retrieve-synchronously url))
(goto-char (point-min))
(unless cached (re-search-forward "^$"))
(prog1
(buffer-substring-no-properties (point) (point-max))
(kill-buffer (current-buffer))))))
(unless (or cached (eq num 0))
(xkcd-cache-json num out))
out))
(defadvice! +xkcd-get (num)
"Get the xkcd number NUM."
:override 'xkcd-get
(interactive "nEnter comic number: ")
(xkcd-update-latest)
(get-buffer-create "*xkcd*")
(switch-to-buffer "*xkcd*")
(xkcd-mode)
(let (buffer-read-only)
(erase-buffer)
(setq xkcd-cur num)
(let* ((xkcd-data (+xkcd-fetch-info num))
(num (plist-get xkcd-data :num))
(img (plist-get xkcd-data :img))
(safe-title (plist-get xkcd-data :safe-title))
(alt (plist-get xkcd-data :alt))
title file)
(message "Getting comic...")
(setq file (xkcd-download img num))
(setq title (format "%d: %s" num safe-title))
(insert (propertize title
'face 'outline-1))
(center-line)
(insert "\n")
(xkcd-insert-image file num)
(if (eq xkcd-cur 0)
(setq xkcd-cur num))
(setq xkcd-alt alt)
(message "%s" title))))
(defconst +xkcd-db--sqlite-available-p
(with-demoted-errors "+org-xkcd initialization: %S"
(emacsql-sqlite-ensure-binary)
t))
(defvar +xkcd-db--connection (make-hash-table :test #'equal)
"Database connection to +org-xkcd database.")
(defun +xkcd-db--get ()
"Return the sqlite db file."
(expand-file-name "xkcd.db" xkcd-cache-dir))
(defun +xkcd-db--get-connection ()
"Return the database connection, if any."
(gethash (file-truename xkcd-cache-dir)
+xkcd-db--connection))
(defconst +xkcd-db--table-schema
'((xkcds
[(num integer :unique :primary-key)
(year :not-null)
(month :not-null)
(link :not-null)
(news :not-null)
(safe_title :not-null)
(title :not-null)
(transcript :not-null)
(alt :not-null)
(img :not-null)])))
(defun +xkcd-db--init (db)
"Initialize database DB with the correct schema and user version."
(emacsql-with-transaction db
(pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema)
(emacsql db [:create-table $i1 $S2] table schema))))
(defun +xkcd-db ()
"Entrypoint to the +org-xkcd sqlite database.
Initializes and stores the database, and the database connection.
Performs a database upgrade when required."
(unless (and (+xkcd-db--get-connection)
(emacsql-live-p (+xkcd-db--get-connection)))
(let* ((db-file (+xkcd-db--get))
(init-db (not (file-exists-p db-file))))
(make-directory (file-name-directory db-file) t)
(let ((conn (emacsql-sqlite db-file)))
(set-process-query-on-exit-flag (emacsql-process conn) nil)
(puthash (file-truename xkcd-cache-dir)
conn
+xkcd-db--connection)
(when init-db
(+xkcd-db--init conn)))))
(+xkcd-db--get-connection))
(defun +xkcd-db-query (sql &rest args)
"Run SQL query on +org-xkcd database with ARGS.
SQL can be either the emacsql vector representation, or a string."
(if (stringp sql)
(emacsql (+xkcd-db) (apply #'format sql args))
(apply #'emacsql (+xkcd-db) sql args)))
(defun +xkcd-db-read (num)
(when-let ((res
(car (+xkcd-db-query [:select * :from xkcds
:where (= num $s1)]
num
:limit 1))))
(+xkcd-db-list-to-plist res)))
(defun +xkcd-db-read-all ()
(let ((xkcd-table (make-hash-table :test 'eql :size 4000)))
(mapcar (lambda (xkcd-info-list)
(puthash (car xkcd-info-list) (+xkcd-db-list-to-plist xkcd-info-list) xkcd-table))
(+xkcd-db-query [:select * :from xkcds]))
xkcd-table))
(defun +xkcd-db-list-to-plist (xkcd-datalist)
`(:num ,(nth 0 xkcd-datalist)
:year ,(nth 1 xkcd-datalist)
:month ,(nth 2 xkcd-datalist)
:link ,(nth 3 xkcd-datalist)
:news ,(nth 4 xkcd-datalist)
:safe-title ,(nth 5 xkcd-datalist)
:title ,(nth 6 xkcd-datalist)
:transcript ,(nth 7 xkcd-datalist)
:alt ,(nth 8 xkcd-datalist)
:img ,(nth 9 xkcd-datalist)))
(defun +xkcd-db-write (data)
(+xkcd-db-query [:insert-into xkcds
:values $v1]
(list (vector
(cdr (assoc 'num data))
(cdr (assoc 'year data))
(cdr (assoc 'month data))
(cdr (assoc 'link data))
(cdr (assoc 'news data))
(cdr (assoc 'safe_title data))
(cdr (assoc 'title data))
(cdr (assoc 'transcript data))
(cdr (assoc 'alt data))
(cdr (assoc 'img data))
)))))
2020-05-16 07:44:01 +00:00
#+END_SRC
** YASnippet
Nested snippets are good, enable that.
#+BEGIN_SRC emacs-lisp
(setq yas-triggers-in-field t)
#+END_SRC
* Language configuration
** General
*** File Templates
For some file types, we overwrite defaults in the [[file:./snippets][snippets]] directory, others
need to have a template assigned.
#+BEGIN_SRC emacs-lisp
(set-file-template! "\\.tex$" :trigger "__" :mode 'latex-mode)
#+END_SRC
2020-04-14 09:12:09 +00:00
** Plaintext
2020-07-19 16:33:28 +00:00
It's nice to see ANSI colour codes displayed
2020-04-14 09:12:09 +00:00
#+BEGIN_SRC emacs-lisp
(after! text-mode
(add-hook! 'text-mode-hook
;; Apply ANSI color codes
(with-silent-modifications
(ansi-color-apply-on-region (point-min) (point-max)))))
#+END_SRC
** Org Mode
2020-04-29 10:32:57 +00:00
:PROPERTIES:
:CUSTOM_ID: org
:END:
2020-05-15 10:07:10 +00:00
I really like org mode, I've given some thought to why, and below is the result.
#+PLOT: transpose:yes type:radar min:0 max:4 file:"misc/document-format-comparison.png"
| Format | Fine-grained-control | Initial Effort | Syntax simplicity | Editor Support | Integrations | Ease-of-referencing | Versatility |
|-------------------+----------------------+----------------+-------------------+----------------+--------------+---------------------+-------------|
| Word | 2 | 4 | 4 | 2 | 3 | 2 | 2 |
| LaTeX | 4 | 1 | 1 | 3 | 2 | 4 | 3 |
| Org Mode | 4 | 2 | 3.5 | 1 | 4 | 4 | 4 |
| Markdown | 1 | 3 | 3 | 4 | 3 | 3 | 1 |
| Markdown + Pandoc | 2.5 | 2.5 | 2.5 | 3 | 3 | 3 | 2 |
2020-08-12 10:21:13 +00:00
#+attr_html: :class invertible :alt Radar chart comparing my opinions of document formats.
2020-08-08 20:15:11 +00:00
[[https://tecosaur.com/lfs/emacs-config/document-format-comparison.png]]
2020-05-15 10:07:10 +00:00
*** System config
Org mode isn't recognised as it's 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~.
#+BEGIN_SRC xml :tangle ~/.local/share/mime/packages/org.xml
<?xml version="1.0" encoding="utf-8"?>
<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>
#+END_SRC
What's nice is that Papirus [[https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/commit/a10fb7f2423d5e30b9c4477416ccdc93c4f3849d][now]] has an icon for =text/org=.
One simply needs to refresh their mime database
2020-03-23 02:59:27 +00:00
#+BEGIN_SRC shell :results silent
update-mime-database ~/.local/share/mime
#+END_SRC
Then set Emacs as the default editor
2020-03-23 02:59:27 +00:00
#+BEGIN_SRC shell :results silent
xdg-mime default emacs.desktop text/org
#+END_SRC
*** Behaviour
[[xkcd:1319]]
2020-03-23 11:07:40 +00:00
**** Tweaking defaults
#+BEGIN_SRC emacs-lisp
(setq org-directory "~/.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 a item is done sounds convininet
org-list-allow-alphabetical t ; have a. A. a) A) list bullets
org-export-in-background t ; run export processes in external emacs process
org-catch-invisible-edits 'smart ; try not to accidently do weird stuff in invisible regions
org-re-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js")
#+END_SRC
2020-03-23 11:07:40 +00:00
I also like the ~:comments~ header-argument, so let's make that a default.
#+BEGIN_SRC emacs-lisp
(setq org-babel-default-header-args '((:session . "none")
(:results . "replace")
(:exports . "code")
(:cache . "no")
(:noweb . "no")
(:hlines . "no")
(:tangle . "no")
(:comments . "link")))
#+END_SRC
By default, ~visual-line-mode~ is turned =on=, and ~auto-fill-mode~ =off= by a hook.
However this messes with tables in Org-mode, and other plaintext files (e.g.
markdown, \LaTeX) so I'll turn it off for this, and manually enable it for more
specific modes as desired.
#+BEGIN_SRC emacs-lisp
(remove-hook 'text-mode-hook #'visual-line-mode)
(add-hook 'text-mode-hook #'auto-fill-mode)
#+END_SRC
2020-06-29 19:13:45 +00:00
There also seem to be a few keybindings which use =hjkl=, but miss arrow key equivalents.
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
2020-03-23 11:07:40 +00:00
**** Extra functionality
***** Org buffer creation
Let's also make creating an org buffer just that little bit easier.
#+BEGIN_SRC emacs-lisp
(evil-define-command evil-buffer-org-new (count file)
"Creates a new ORG buffer replacing the current window, optionally
editing a certain FILE"
:repeat nil
(interactive "P<f>")
(if file
(evil-edit file)
(let ((buffer (generate-new-buffer "*new org*")))
(set-window-buffer nil buffer)
(with-current-buffer buffer
(org-mode)))))
(map! :leader
(:prefix "b"
:desc "New empty ORG buffer" "o" #'evil-buffer-org-new))
#+END_SRC
***** List bullet sequence
2020-02-29 10:46:14 +00:00
I think it makes sense to have list bullets change with depth
#+BEGIN_SRC emacs-lisp
(setq org-list-demote-modify-bullet '(("+" . "-") ("-" . "+") ("*" . "+") ("1." . "a.")))
2020-02-29 10:46:14 +00:00
#+END_SRC
***** Citation
2020-02-26 11:05:41 +00:00
Occasionally I want to cite something.
#+BEGIN_SRC emacs-lisp
(use-package! org-ref
2020-02-26 11:05:41 +00:00
:after org
:config
(setq org-ref-completion-library 'org-ref-ivy-cite))
#+END_SRC
***** cdlatex
It's also nice to be able to use ~cdlatex~.
#+BEGIN_SRC emacs-lisp
(after! org (add-hook 'org-mode-hook 'turn-on-org-cdlatex))
#+END_SRC
It's handy to be able to quickly insert environments with =C-c }=. I almost always
want to edit them afterwards though, so let's make that happen by default.
#+BEGIN_SRC emacs-lisp
(after! org
(defadvice! org-edit-latex-emv-after-insert ()
:after #'org-cdlatex-environment-indent
(org-edit-latex-environment)))
#+END_SRC
At some point in the future it could be good to investigate [[https://scripter.co/splitting-an-org-block-into-two/][splitting org blocks]].
Likewise [[https://archive.casouri.cat/note/2020/insert-math-symbol-in-emacs/][this]] looks good for symbols.
***** Spellcheck
2020-03-23 03:01:31 +00:00
My spelling is atrocious, so let's get flycheck going.
#+BEGIN_SRC emacs-lisp
(after! org (add-hook 'org-mode-hook 'turn-on-flyspell))
#+END_SRC
***** LSP support in ~src~ blocks
Now, by default, LSPs don't really function at all in ~src~ blocks.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
***** View exported file
='localeader v= has no pre-existing binding, so I may as well use it with the same
functionality as in LaTeX. Let's try viewing possible output files with this.
#+BEGIN_SRC emacs-lisp
(after! org
(map! :map org-mode-map
:localleader
:desc "View exported file" "v" #'org-view-output-file)
(defun org-view-output-file (&optional org-file-path)
"Visit buffer open on the first output file (if any) found, using `org-view-output-file-extensions'"
(interactive)
(let* ((org-file-path (or org-file-path (buffer-file-name) ""))
(dir (file-name-directory org-file-path))
(basename (file-name-base org-file-path))
(output-file nil))
(dolist (ext org-view-output-file-extensions)
(unless output-file
(when (file-exists-p
(concat dir basename "." ext))
(setq output-file (concat dir basename "." ext)))))
(if output-file
(if (member (file-name-extension output-file) org-view-external-file-extensions)
(browse-url-xdg-open output-file)
(pop-to-buffer (or (find-buffer-visiting output-file)
(find-file-noselect output-file))))
(message "No exported file found")))))
(defvar org-view-output-file-extensions '("pdf" "md" "rst" "txt" "tex" "html")
"Search for output files with these extensions, in order, viewing the first that matches")
(defvar org-view-external-file-extensions '("html")
"File formats that should be opened externally.")
#+END_SRC
**** Super agenda
#+BEGIN_SRC emacs-lisp
(use-package! org-super-agenda
:commands (org-super-agenda-mode))
(after! org-agenda
(org-super-agenda-mode))
(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 "WAITING"
:order 20)
(:name "University"
:tag "uni"
:order 32)
(:name "Trivial"
:priority<= "E"
:tag ("Trivial" "Unimportant")
:todo ("SOMEDAY" )
:order 90)
(:discard (:tag ("Chore" "Routine" "Daily")))))))))))
#+END_SRC
**** Capture
2020-08-29 15:12:39 +00:00
Let's setup some org-capture templates, and make them visually nice to access.
#+attr_html: :class invertible :alt My org-capture dialouge.
[[https://tecosaur.com/lfs/emacs-config/screenshots/org-capture.png]]
#+BEGIN_SRC emacs-lisp :noweb yes
(use-package! doct
:commands (doct))
(after! org-capture
<<prettify-capture>>
(setq +org-capture-uni-units (split-string (f-read-text "~/.org/.uni-units")))
(setq +org-capture-recipies "~/Desktop/TEC/Organisation/recipies.org")
(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))
(add-transient-hook! 'org-capture-select-template
(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")
)
("University" :keys "u"
:icon ("graduation-cap" :set "faicon" :color "purple")
:file +org-capture-todo-file
:headline "University"
:unit-prompt ,(format "%%^{Unit|%s}" (string-join +org-capture-uni-units "|"))
:prepend t
:type entry
:children (("Test" :keys "t"
:icon ("timer" :set "material" :color "red")
:template ("* TODO [#C] %{unit-prompt} %? :uni:tests:"
"SCHEDULED: %^{Test date:}T"
"%i %a"))
("Assignment" :keys "a"
:icon ("library_books" :set "material" :color "orange")
:template ("* TODO [#B] %{unit-prompt} %? :uni:assignments:"
"DEADLINE: %^{Due date:}T"
"%i %a"))
("Lecture" :keys "l"
:icon ("keynote" :set "fileicon" :color "orange")
:template ("* TODO [#C] %{unit-prompt} %? :uni:lecture:"
"%i %a"))
("Miscellaneous task" :keys "u"
:icon ("list" :set "faicon" :color "yellow")
:template ("* TODO [#D] %{unit-prompt} %? :uni:"
"%i %a"))))
("Email" :keys "e"
:icon ("envelope" :set "faicon" :color "blue")
:file +org-capture-todo-file
:prepend t
:headline "Inbox"
:type entry
2020-04-13 19:09:13 +00:00
:template ("* TODO %^{type|reply to|contact} %\\3 %? :email:"
"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"
)
("\tRecipie" :keys "r"
:icon ("spoon" :set "faicon" :color "dorange")
:file +org-capture-recipies
:headline "Unsorted"
:template "%(org-chef-get-recipe-from-url)"
)
("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}"
2020-04-20 08:00:48 +00:00
"%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))
))))))
#+END_SRC
It would also be nice to improve how the capture dialogue looks
#+NAME: prettify-capture
#+BEGIN_SRC emacs-lisp :tangle no
(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 sub-level 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)))
(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)
#+END_SRC
The [[file:~/.emacs.d/bin/org-capture][org-capture bin]] is rather nice, but I'd be nicer with a smaller frame, and
no modeline.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
2020-04-28 18:41:46 +00:00
**** Roam
***** Basic settings
I'll just set this to be within =Organisation= folder for now, in the future it
could be worth seeing if I could hook this up to a [[https://nextcloud.com/][Nextcloud]] instance.
#+BEGIN_SRC emacs-lisp
(setq org-roam-directory "~/Desktop/TEC/Organisation/Roam/")
2020-04-28 18:41:46 +00:00
#+END_SRC
***** Registering roam protocol
2020-06-02 05:06:47 +00:00
The recommended method of registering a protocol is by registering a desktop
2020-04-28 18:41:46 +00:00
application, which seems reasonable.
#+BEGIN_SRC conf :tangle ~/.local/share/applications/org-protocol.desktop :mkdirp yes
2020-04-28 18:41:46 +00:00
[Desktop Entry]
Name=Org-Protocol
Exec=emacsclient %u
Icon=emacs-icon
Type=Application
Terminal=false
MimeType=x-scheme-handler/org-protocol
#+END_SRC
To associate ~org-protocol://~ links with the desktop file,
#+BEGIN_SRC shell
xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol
#+END_SRC
***** Graph Behaviour
2020-05-09 18:48:36 +00:00
By default, clicking on an ~org-protocol://~ link messes with the =svg= view. To fix
this we can use an ~iframe~, however that requires shifting to an =html= file.
Hence, we need to do a bit of overriding.
#+BEGIN_SRC html :tangle misc/org-roam-template.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Roam Graph</title>
<meta name="viewport" content="width=device-width">
<style type="text/css">
2020-06-29 19:58:12 +00:00
body {
background: white;
}
svg {
position: relative;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
2020-06-29 19:58:12 +00:00
width: 95vw;
}
a > polygon {
transition-duration: 200ms;
transition-property: fill;
}
a > polyline {
transition-duration: 400ms;
transition-property: stroke;
}
a:hover > polygon {
fill: #d4d4d4;
}
a:hover > polyline {
stroke: #888;
}
</style>
2020-08-29 15:32:00 +00:00
<script>
function create_iframe (url) {
i = document.createElement('iframe');
i.setAttribute('src', url);
i.style.setProperty('display', 'none');
document.body.append(i);
}
function listen_on_all_a () {
document.querySelectorAll("svg a").forEach(elem => {
elem.addEventListener('click', (e) => {
e.preventDefault();
create_iframe(elem.href.baseVal);
});
});
}
</script>
</head>
<body onload="listen_on_all_a()">
%s
</body>
</html>
#+END_SRC
#+BEGIN_SRC emacs-lisp
(after! org-roam
2020-06-29 19:58:12 +00:00
(setq org-roam-graph-node-extra-config '(("shape" . "underline")
("style" . "rounded,filled")
("fillcolor" . "#EEEEEE")
("color" . "#C9C9C9")
("fontcolor" . "#111111")
("fontname" . "Overpass")))
(setq +org-roam-graph--html-template
(replace-regexp-in-string "%\\([^s]\\)" "%%\\1"
(f-read-text (concat doom-private-dir "misc/org-roam-template.html"))))
(defadvice! +org-roam-graph--build-html (&optional node-query callback)
"Generate a graph showing the relations between nodes in NODE-QUERY. HTML style."
:override #'org-roam-graph--build
2020-06-29 19:58:12 +00:00
(unless (stringp org-roam-graph-executable)
(user-error "`org-roam-graph-executable' is not a string"))
(unless (executable-find org-roam-graph-executable)
(user-error (concat "Cannot find executable %s to generate the graph. "
"Please adjust `org-roam-graph-executable'")
org-roam-graph-executable))
(let* ((node-query (or node-query
2020-06-29 19:58:12 +00:00
`[:select [file titles] :from titles
,@(org-roam-graph--expand-matcher 'file t)]))
(graph (org-roam-graph--dot node-query))
(temp-dot (make-temp-file "graph." nil ".dot" graph))
(temp-graph (make-temp-file "graph." nil ".svg"))
(temp-html (make-temp-file "graph." nil ".html")))
2020-06-29 19:58:12 +00:00
(org-roam-message "building graph")
(make-process
:name "*org-roam-graph--build-process*"
:buffer "*org-roam-graph--build-process*"
:command `(,org-roam-graph-executable ,temp-dot "-Tsvg" "-o" ,temp-graph)
:sentinel (progn
(lambda (process _event)
(when (= 0 (process-exit-status process))
(write-region (format +org-roam-graph--html-template (f-read-text temp-graph)) nil temp-html)
(when callback
(funcall callback temp-html)))))))))
2020-04-28 18:41:46 +00:00
#+END_SRC
***** Modeline file name
All those numbers! It's messy. Let's adjust this in a similar way that I have in
the[[*Window title][Window title]].
#+BEGIN_SRC emacs-lisp
(defadvice! doom-modeline--reformat-roam (orig-fun)
:around #'doom-modeline-buffer-file-name
(message "Reformat?")
(message (buffer-file-name))
(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) "
(funcall orig-fun))
(funcall orig-fun)))
#+END_SRC
**** Nicer generated heading IDs
Thanks to alphapapa's [[https://github.com/alphapapa/unpackaged.el#export-to-html-with-useful-anchors][unpackaged.el]].
By default, ~url-hexify-string~ seemed to cause me some issues. Replacing that in
~a53899~ resolved this for me. To go one step further, I create a function for
producing nice short links, like an inferior version of ~reftex-label~.
#+BEGIN_SRC emacs-lisp
(defvar org-heading-contraction-max-words 3
"Maximum number of words in a heading")
(defvar org-heading-contraction-max-length 35
"Maximum length of resulting string")
(defvar org-heading-contraction-stripped-words
'("the" "on" "in" "off" "a" "for" "by" "of" "and" "is" "to")
"Unnecesary words to be removed from a heading")
(defun org-heading-contraction (heading-string)
"Get a contracted form of HEADING-STRING that is onlu contains alphanumeric charachters.
Strips 'joining' words in `org-heading-contraction-stripped-words',
and then limits the result to the first `org-heading-contraction-max-words' words.
If the total length is > `org-heading-contraction-max-length' then individual words are
truncated to fit within the limit"
(let ((heading-words
(-filter (lambda (word)
(not (member word org-heading-contraction-stripped-words)))
(split-string
(->> heading-string
s-downcase
(replace-regexp-in-string "\\[\\[[^]]+\\]\\[\\([^]]+\\)\\]\\]" "\\1") ; get description from org-link
(replace-regexp-in-string "[-/ ]+" " ") ; replace seperator-type chars with space
(replace-regexp-in-string "[^a-z0-9 ]" "") ; strip chars which need %-encoding in a uri
) " "))))
(when (> (length heading-words)
org-heading-contraction-max-words)
(setq heading-words
(subseq heading-words 0 org-heading-contraction-max-words)))
(when (> (+ (-sum (mapcar #'length heading-words))
(1- (length heading-words)))
org-heading-contraction-max-length)
;; trucate each word to a max word length determined by
;; max length = \floor{ \frac{total length - chars for seperators - \sum_{word \leq average length} length(word) }{num(words) > average length} }
(setq heading-words (let* ((total-length-budget (- org-heading-contraction-max-length ; how many non-separator chars we can use
(1- (length heading-words))))
(word-length-budget (/ total-length-budget ; max length of each word to keep within budget
org-heading-contraction-max-words))
(num-overlong (-count (lambda (word) ; how many words exceed that budget
(> (length word) word-length-budget))
heading-words))
(total-short-length (-sum (mapcar (lambda (word) ; total length of words under that budget
(if (<= (length word) word-length-budget)
(length word) 0))
heading-words)))
(max-length (/ (- total-length-budget total-short-length) ; max(max-length) that we can have to fit within the budget
num-overlong)))
(mapcar (lambda (word)
(if (<= (length word) max-length)
word
(substring word 0 max-length)))
heading-words))))
(string-join heading-words "-")))
#+END_SRC
Now here's alphapapa's subtly tweaked mode.
#+BEGIN_SRC emacs-lisp
(define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
"Attempt to export Org as HTML with useful link IDs.
Instead of random IDs like \"#orga1b2c3\", use heading titles,
made unique when necessary."
:global t
(if unpackaged/org-export-html-with-useful-ids-mode
(advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
(advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)))
(defun unpackaged/org-export-get-reference (datum info)
"Like `org-export-get-reference', except uses heading titles instead of random numbers."
(let ((cache (plist-get info :internal-references)))
(or (car (rassq datum cache))
(let* ((crossrefs (plist-get info :crossrefs))
(cells (org-export-search-cells datum))
;; Preserve any pre-existing association between
;; a search cell and a reference, i.e., when some
;; previously published document referenced a location
;; within current file (see
;; `org-publish-resolve-external-link').
;;
;; However, there is no guarantee that search cells are
;; unique, e.g., there might be duplicate custom ID or
;; two headings with the same title in the file.
;;
;; As a consequence, before re-using any reference to
;; an element or object, we check that it doesn't refer
;; to a previous element or object.
(new (or (cl-some
(lambda (cell)
(let ((stored (cdr (assoc cell crossrefs))))
(when stored
(let ((old (org-export-format-reference stored)))
(and (not (assoc old cache)) stored)))))
cells)
(when (org-element-property :raw-value datum)
;; Heading with a title
(unpackaged/org-export-new-named-reference datum cache))
(when (member (car datum) '(src-block table example fixed-width property-drawer))
;; Nameable elements
(unpackaged/org-export-new-named-reference datum cache))
;; NOTE: This probably breaks some Org Export
;; feature, but if it does what I need, fine.
(org-export-format-reference
(org-export-new-reference cache))))
(reference-string new))
;; Cache contains both data already associated to
;; a reference and in-use internal references, so as to make
;; unique references.
(dolist (cell cells) (push (cons cell new) cache))
;; Retain a direct association between reference string and
;; DATUM since (1) not every object or element can be given
;; a search cell (2) it permits quick lookup.
(push (cons reference-string datum) cache)
(plist-put info :internal-references cache)
reference-string))))
(defun unpackaged/org-export-new-named-reference (datum cache)
"Return new reference for DATUM that is unique in CACHE."
(cl-macrolet ((inc-suffixf (place)
`(progn
(string-match (rx bos
(minimal-match (group (1+ anything)))
(optional "--" (group (1+ digit)))
eos)
,place)
;; HACK: `s1' instead of a gensym.
(-let* (((s1 suffix) (list (match-string 1 ,place)
(match-string 2 ,place)))
(suffix (if suffix
(string-to-number suffix)
0)))
(setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
(let* ((headline-p (eq (car datum) 'headline))
(title (if headline-p
(org-element-property :raw-value datum)
(or (org-element-property :name datum)
(concat (org-element-property :raw-value
(org-element-property :parent
(org-element-property :parent datum)))))))
;; get ascii-only form of title without needing percent-encoding
(ref (concat (org-heading-contraction (substring-no-properties title))
(unless (or headline-p (org-element-property :name datum))
(concat ","
(case (car datum)
('src-block "code")
('example "example")
('fixed-width "mono")
('property-drawer "properties")
(t (symbol-name (car datum))))
"--1"))))
(parent (when headline-p (org-element-property :parent datum))))
(while (--any (equal ref (car it))
cache)
;; Title not unique: make it so.
(if parent
;; Append ancestor title.
(setf title (concat (org-element-property :raw-value parent)
"--" title)
;; get ascii-only form of title without needing percent-encoding
ref (org-heading-contraction (substring-no-properties title))
parent (when headline-p (org-element-property :parent parent)))
;; No more ancestors: add and increment a number.
(inc-suffixf ref)))
ref)))
(add-hook 'org-load-hook #'unpackaged/org-export-html-with-useful-ids-mode)
#+END_SRC
**** Nicer ~org-return~
Once again, from [[https://github.com/alphapapa/unpackaged.el#org-return-dwim][unpackaged.el]]
#+BEGIN_SRC emacs-lisp
(after! org
(defun unpackaged/org-element-descendant-of (type element)
"Return non-nil if ELEMENT is a descendant of TYPE.
TYPE should be an element type, like `item' or `paragraph'.
ELEMENT should be a list like that returned by `org-element-context'."
;; MAYBE: Use `org-element-lineage'.
(when-let* ((parent (org-element-property :parent element)))
(or (eq type (car parent))
(unpackaged/org-element-descendant-of type parent))))
;;;###autoload
(defun unpackaged/org-return-dwim (&optional default)
"A helpful replacement for `org-return-indent'. With prefix, call `org-return-indent'.
On headings, move point to position after entry content. In
lists, insert a new item or end the list, with checkbox if
appropriate. In tables, insert a new row or end the table."
;; Inspired by John Kitchin: http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/
(interactive "P")
(if default
(org-return t)
(cond
;; Act depending on context around point.
;; NOTE: I prefer RET to not follow links, but by uncommenting this block, links will be
;; followed.
;; ((eq 'link (car (org-element-context)))
;; ;; Link: Open it.
;; (org-open-at-point-global))
((org-at-heading-p)
;; Heading: Move to position after entry content.
;; NOTE: This is probably the most interesting feature of this function.
(let ((heading-start (org-entry-beginning-position)))
(goto-char (org-entry-end-position))
(cond ((and (org-at-heading-p)
(= heading-start (org-entry-beginning-position)))
;; Entry ends on its heading; add newline after
(end-of-line)
(insert "\n\n"))
(t
;; Entry ends after its heading; back up
(forward-line -1)
(end-of-line)
(when (org-at-heading-p)
;; At the same heading
(forward-line)
(insert "\n")
(forward-line -1))
;; FIXME: looking-back is supposed to be called with more arguments.
(while (not (looking-back (rx (repeat 3 (seq (optional blank) "\n")))))
(insert "\n"))
(forward-line -1)))))
((org-at-item-checkbox-p)
;; Checkbox: Insert new item with checkbox.
(org-insert-todo-heading nil))
((org-in-item-p)
;; Plain list. Yes, this gets a little complicated...
(let ((context (org-element-context)))
(if (or (eq 'plain-list (car context)) ; First item in list
(and (eq 'item (car context))
(not (eq (org-element-property :contents-begin context)
(org-element-property :contents-end context))))
(unpackaged/org-element-descendant-of 'item context)) ; Element in list item, e.g. a link
;; Non-empty item: Add new item.
(org-insert-item)
;; Empty item: Close the list.
;; TODO: Do this with org functions rather than operating on the text. Can't seem to find the right function.
(delete-region (line-beginning-position) (line-end-position))
(insert "\n"))))
((when (fboundp 'org-inlinetask-in-task-p)
(org-inlinetask-in-task-p))
;; Inline task: Don't insert a new heading.
(org-return t))
((org-at-table-p)
(cond ((save-excursion
(beginning-of-line)
;; See `org-table-next-field'.
(cl-loop with end = (line-end-position)
for cell = (org-element-table-cell-parser)
always (equal (org-element-property :contents-begin cell)
(org-element-property :contents-end cell))
while (re-search-forward "|" end t)))
;; Empty row: end the table.
(delete-region (line-beginning-position) (line-end-position))
(org-return t))
(t
;; Non-empty row: call `org-return-indent'.
(org-return t))))
(t
;; All other cases: call `org-return-indent'.
2020-06-06 04:57:02 +00:00
(org-return t))))))
(map!
:after evil-org
:map evil-org-mode-map
:i [return] #'unpackaged/org-return-dwim)
#+END_SRC
2020-08-18 17:44:59 +00:00
**** Snippet Helper
For snippets which want to depend on the ~#+THING:~ on the current line.
This is mostly source blocks, and property args, so let's get fancy with them.
One-letter snippets are super-convenient, but for them to not be a pain
everywhere else we'll need a nice condition function to use in yasnippet.
By having this function give slightly more than a simple ~t~ or ~nil~, we can
use in a second function to get the most popular language without explicit
global header args.
#+BEGIN_SRC emacs-lisp
(defun +yas/org-src-lang ()
"Try to find the current language of the src/header at point.
Return nil otherwise."
(save-excursion
(pcase
(downcase
(buffer-substring-no-properties
(goto-char (line-beginning-position))
(or (ignore-errors (1- (search-forward " " (line-end-position))))
(1+ (point)))))
("#+property:"
(when (re-search-forward "header-args:")
(buffer-substring-no-properties
(point)
(or (and (forward-word) (point))
(1+ (point))))))
("#+begin_src"
(buffer-substring-no-properties
(point)
(or (and (forward-word) (point))
(1+ (point)))))
("#+header:"
(search-forward "#+begin_src")
(+yas/org-src-lang))
(t nil))))
(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 (search-forward "#+begin_src" nil t)
(push (+yas/org-src-lang) src-langs))
(goto-char (point-min))
(while (re-search-forward "#\\+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 (set-difference src-langs header-langs :test #'string=))))
#+END_SRC
2020-05-15 10:07:10 +00:00
**** Org Plot
There are two main bits of extra functionality I wan to add
+ the ability to transpose tables (internally)
+ a radar type
To accomplish this, overriding the ~org-plot/gnuplot~ function seems to be the
clear way. I tried this, and overrode so much that it seemed to make sense
just to modify =org-plot.el= instead, as it was only ~350 lines and I functions
that encompassed most of that line count.
Given that my [[file:lisp/org-plot.el][org-plot.el]] is now ~700 lines, this seems to have been a good call.
For transposition, I've added the following (equivalent) keys
+ ~[transpose|t]~
+ ~t~
When set to ~yes~, ~y~, or ~t~ the table is internally transposed (i.e. the =elisp= data
form of the table only).
The radar chart is a lot more involved, and I've added the following keys
+ ~type:radar~
This uses the radar template, the result of which can be seen in [[#org][Org Mode]].
2020-05-15 10:07:10 +00:00
+ ~[min|ymin]:0..n~
This sets the start value of /every/ axis.
+ ~[max|ymax]:0..n~
This sets the end value of /every/ axis.
If both ~ymin~ and ~ymax~ are set, only the first axis gets tick labels.
+ ~ticks:0..n~
This sets the number of ticks. Only works for \(n>2\), or \(n=0\) in which
case no ticks are shown.
**** Extra links
***** xkcd
Because xkcd is cool, let's make it as easy and fun as possible to insert them.
Saving seconds adds up after all! (but only so much)
[[xkcd:1205]]
#+BEGIN_SRC emacs-lisp
(after! org
(org-link-set-parameters "xkcd"
:image-data-fun #'+org-xkcd-image-fn
:follow #'+org-xkcd-open-fn
:export #'+org-xkcd-export
:complete #'+org-xkcd-complete)
(defun +org-xkcd-open-fn (link)
(+org-xkcd-image-fn nil link nil))
2020-05-16 07:44:01 +00:00
(defun +org-xkcd-image-fn (protocol link description)
"Get image data for xkcd num LINK"
(let* ((xkcd-info (+xkcd-fetch-info (string-to-number link)))
2020-05-16 07:44:01 +00:00
(img (plist-get xkcd-info :img))
(alt (plist-get xkcd-info :alt)))
(message alt)
(+org-image-file-data-fn protocol (xkcd-download img (string-to-number link)) description)))
(defun +org-xkcd-export (num desc backend _com)
2020-05-16 07:44:01 +00:00
"Convert xkcd to html/LaTeX form"
(let* ((xkcd-info (+xkcd-fetch-info (string-to-number num)))
2020-05-16 07:44:01 +00:00
(img (plist-get xkcd-info :img))
(alt (plist-get xkcd-info :alt))
(title (plist-get xkcd-info :title))
(file (xkcd-download img (string-to-number num))))
2020-05-16 07:44:01 +00:00
(cond ((org-export-derived-backend-p backend 'html)
2020-08-12 10:21:13 +00:00
(format "<img class='invertible' src='%s' title=\"%s\" alt='%s'>" img (subst-char-in-string ?\" ?“ alt) title))
2020-05-16 07:44:01 +00:00
((org-export-derived-backend-p backend 'latex)
(format "\\begin{figure}[!htb]
\\centering
\\includegraphics[scale=0.4]{%s}%s
\\end{figure}" file (if (equal desc (format "xkcd:%s" num)) ""
(format "\n \\caption*{\\label{xkcd:%s} %s}"
num
(or desc
(format "\\textbf{%s} %s" title alt))))))
(t (format "https://xkcd.com/%s" num)))))
2020-05-16 07:44:01 +00:00
(defun +org-xkcd-complete (&optional arg)
"Complete xkcd using `+xkcd-stored-info'"
(format "xkcd:%d" (+xkcd-select))))
#+END_SRC
***** Music
First, we set up all the necessarily 'utility' functions.
#+BEGIN_SRC emacs-lisp
(after! org
(defvar org-music-player 'mpris
"Music player type. Curretly only supports mpris.")
(defvar org-music-mpris-player "Lollypop"
"Name of the mpris player, used in the form org.gnome.MPRIS.")
(defvar org-music-track-search-method 'beets
"Method to find the track file from the link.")
(defvar org-music-beets-db "~/Music/library.db"
"Location of the beets DB, for when using beets as the `org-music-track-search-method'")
(defvar org-music-folder "~/Music/"
"Location of your music folder, for when using file as the `org-music-track-search-method'")
(defvar org-music-recognised-extensions '("flac" "mp4" "m4a" "aiff" "wav" "ogg" "aiff")
"When searching for files in `org-music-track-search-method', recognise these extensions as audio files.")
(defun org-music-get-link (full &optional include-time)
"Generate link string for currently playing track, optionally including a time-stamp"
(case org-music-player ;; NOTE this could do with better generalisation
('mpris (let* ((track-metadata
(org-music-mpris-get-property "Metadata"))
(album-artist (caar (cadr (assoc "xesam:albumArtist" track-metadata))))
(artist (if (or (equal album-artist "")
(s-contains-p "various" album-artist t))
(caar (cadr (assoc "xesam:artist" track-metadata)))
album-artist))
(track (car (cadr (assoc "xesam:title" track-metadata))))
(start-time (when include-time
(/ (org-music-mpris-get-property "Position") 1000000))))
(if full
(format "[[music:%s][%s by %s]]" (org-music-format-link artist track start-time) track artist)
(org-music-format-link artist track start-time))))
(t (user-error! "The specified music player: %s is not supported" org-music-player))))
(defun org-music-format-link (artist track &optional start-time end-time)
(let ((artist (replace-regexp-in-string ":" "\\:" artist))
(track (replace-regexp-in-string ":" "\\:" track)))
(concat artist ":" track
(cond ((and start-time end-time)
(format "::%s-%s"
(org-music-seconds-to-time start-time)
(org-music-seconds-to-time end-time)))
(start-time
(format "::%s"
(org-music-seconds-to-time start-time)))))))
(defun org-music-parse-link (link)
(let* ((link-dc (->> link
(replace-regexp-in-string "\\([^\\\\]\\)\\\\:" "\\1#COLON#")
(replace-regexp-in-string "\\(::[a-z0-9]*[0-9]\\)\\'" "\\1s")))
(link-components (mapcar (lambda (lc) (replace-regexp-in-string "#COLON#" ":" lc))
(s-split ":" link-dc)))
(artist (nth 0 link-components))
(track (nth 1 link-components))
(durations (when (and (> (length link-components) 3)
(equal (nth 2 link-components) ""))
(s-split "-" (nth 3 link-components))))
(start-time (when durations
(org-music-time-to-seconds (car durations))))
(end-time (when (cdr durations)
(org-music-time-to-seconds (cadr durations)))))
(list artist track start-time end-time)))
(defun org-music-seconds-to-time (seconds)
"Convert a number of seconds to a nice human duration, e.g. 5m21s.
This action is reversed by `org-music-time-to-seconds'."
(if (< seconds 60)
(format "%ss" seconds)
(if (< seconds 3600)
(format "%sm%ss" (/ seconds 60) (% seconds 60))
(format "%sh%sm%ss" (/ seconds 3600) (/ (% seconds 3600) 60) (% seconds 60)))))
(defun org-music-time-to-seconds (time-str)
"Get the number of seconds in a string produced by `org-music-seconds-to-time'."
(let* ((time-components (reverse (s-split "[a-z]" time-str)))
(seconds (string-to-number (nth 1 time-components)))
(minutes (when (> (length time-components) 2)
(string-to-number (nth 2 time-components))))
(hours (when (> (length time-components) 3)
(string-to-number (nth 3 time-components)))))
(+ (* 3600 (or hours 0)) (* 60 (or minutes 0)) seconds)))
(defun org-music-play-track (artist title &optional start-time end-time)
"Play the track specified by ARTIST and TITLE, optionally skipping to START-TIME in, stopping at END-TIME."
(if-let ((file (org-music-find-track-file artist title)))
(case org-music-player
('mpris (org-music-mpris-play file start-time end-time))
(t (user-error! "The specified music player: %s is not supported" org-music-player)))
(user-error! "Could not find the track '%s' by '%s'" title artist)))
(add-transient-hook! #'org-music-play-track
(require 'dbus))
(defun org-music-mpris-play (file &optional start-time end-time)
(let ((uri (url-encode-url (rng-file-name-uri file))))
(org-music-mpris-call-method "OpenUri" uri)
(let ((track-id (caadr (assoc "mpris:trackid"
(org-music-mpris-get-property "Metadata")))))
(when start-time
(org-music-mpris-call-method "SetPosition" :object-path track-id
:int64 (round (* start-time 1000000))))
(when end-time
(org-music-mpris-stop-at-time uri end-time)))))
(defun orgb3-music-mpris-stop-at-time (url end-time)
"Check that url is playing, and if it is stop it at END-TIME."
(when (equal url (caadr (assoc "xesam:url" (org-music-mpris-get-property "Metadata"))))
(let* ((time-current (/ (/ (org-music-mpris-get-property "Position") 10000) 100.0))
(time-delta (- end-time time-current)))
(message "%s" time-delta)
(if (< time-delta 0)
(org-music-mpris-call-method "Pause")
(if (< time-delta 6)
(run-at-time (max 0.001 (* 0.9 time-delta)) nil #'org-music-mpris-stop-at-time url end-time)
(run-at-time 5 nil #'org-music-mpris-stop-at-time url end-time))))))
(defun org-music-mpris-get-property (property)
"Return the value of org.mpris.MediaPlayer2.Player.PROPERTY."
(dbus-get-property :session (concat "org.gnome." org-music-mpris-player)
"/org/mpris/MediaPlayer2" "org.mpris.MediaPlayer2.Player"
property))
(defun org-music-mpris-call-method (property &rest args)
"Call org.mpris.MediaPlayer2.Player.PROPERTY with ARGS, returning the result."
(apply #'dbus-call-method :session (concat "org.gnome." org-music-mpris-player)
"/org/mpris/MediaPlayer2" "org.mpris.MediaPlayer2.Player"
property args))
(defun org-music-guess-mpris-player ()
(when-let ((players
(-filter (lambda (interface)
(s-contains-p "org.mpris.MediaPlayer2" interface))
(dbus-call-method :session
dbus-service-dbus
dbus-path-dbus
dbus-interface-dbus
"ListNames"))))
(replace-regexp-in-string "org\\.mpris\\.MediaPlayer2\\." "" (car players))))
(when (eq org-music-player 'mpris)
(unless org-music-mpris-player
(setq org-music-mpris-player (org-music-guess-mpris-player))))
(defun org-music-find-track-file (artist title)
"Try to find the file for TRACK by ARTIST, using `org-music-track-search-method', returning nil if nothing could be found."
(case org-music-track-search-method
('file (org-music-find-file artist title))
('beets (org-music-beets-find-file artist title))
(t (user-error! "The specified music search method: %s is not supported" org-music-track-search-method))))
(defun org-music-beets-find-file (artist title)
"Find the file correspanding to a given artist and title."
(let* ((artist-escaped (replace-regexp-in-string "\"" "\\\"" artist))
(title-escaped (replace-regexp-in-string "\"" "\\\"" title))
(file
(or
(shell-command-to-string
(format
"sqlite3 '%s' \"SELECT path FROM items WHERE albumartist IS '%s' AND title IS '%s' LIMIT 1 COLLATE NOCASE\""
(expand-file-name org-music-beets-db) artist-escaped title-escaped))
(shell-command-to-string
(format
"sqlite3 '%s' \"SELECT path FROM items WHERE artist IS '%s' AND title IS '%s' LIMIT 1 COLLATE NOCASE\""
(expand-file-name org-music-beets-db) artist-escaped title-escaped)))))
(if (> (length file) 0)
(substring file 0 -1)
)))
(defun org-music-find-file (artist title)
"Try to find a file in `org-music-folder' which contains TITLE, looking first in ./ARTIST if possible."
(when-let* ((music-folder (expand-file-name org-music-folder))
(search-folders (or
(-filter ; look for folders which contain ARTIST
(lambda (file-or-folder)
(and
(s-contains-p artist (file-name-base file-or-folder) t)
(file-directory-p file-or-folder)))
(directory-files music-folder t))
(list music-folder)))
(extension-regex (format "\\.\\(?:%s\\)\\'" (s-join "\\|" org-music-recognised-extensions)))
(tracks (-filter
(lambda (file)
(s-contains-p title (file-name-base file) t))
(-flatten (mapcar (lambda (dir)
(directory-files-recursively dir extension-regex))
search-folders)))))
(when (> (length tracks) 1)
(message "Warning: multiple matches for %s by %s found" title artist))
(car tracks))))
#+END_SRC
Then we integrate this nicely with org-mode
#+BEGIN_SRC emacs-lisp
(after! org
(org-link-set-parameters "music"
:follow #'org-music-open-fn
:export #'org-music-export-text)
(org-link-set-parameters "Music" ;; like music, but visually fancier
;; FIXME this should work as far as I can tell
;; :image-data-fun #'org-music-image-fn
:follow #'org-music-open-fn
:export #'org-music-fancy-export)
(defun org-music-open-fn (link)
(apply #'org-music-play-track (org-music-parse-link link)))
(defun org-music-insert-current-track (&optional include-time)
"Insert link to currest track, including a timestamp when the universal argument is supplied."
(interactive "P")
(pp include-time)
(insert (org-music-get-link t include-time)))
(defun org-music-export-text (path desc backend _com &optional newline)
(let* ((track-info (org-music-parse-link path))
(artist (nth 0 track-info))
(track (nth 1 track-info))
(start-time (nth 2 track-info))
(end-time (nth 3 track-info))
(emphasise (cond ((org-export-derived-backend-p backend 'html)
(lambda (s) (format "<span style=\"font-style: italic\">%s</span>" s)))
((org-export-derived-backend-p backend 'latex)
(lambda (s) (format "\\emph{%s}" s)))
(t (lambda (s) s)))))
(or desc
(concat
(cond ((and start-time end-time)
(format "%s to %s seconds of%s" start-time end-time (or newline " ")))
(start-time
(format "%s seconds into%s" start-time (or newline " "))))
(funcall emphasise track)
(or newline " ")
"by "
artist))))
(defun org-music-cover-image (track-file)
"Try to find a cover image for the track in the given location"
(car (-filter (lambda (file)
(-contains-p '("png" "jpg" "jpeg") (file-name-extension file)))
(directory-files (file-name-directory track-file) t "cover"))))
2020-06-29 18:57:26 +00:00
(defun org-music-image-fn (_protocol link _description)
(when-let* ((track-data (org-music-parse-link link))
(cover-file (org-music-cover-image
(org-music-find-track-file
(nth 0 track-data) (nth 1 track-data)))))
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(insert-file-contents-literally cover-file)
(buffer-substring-no-properties (point-min) (point-max)))))
(defun org-music-fancy-export (path desc backend _com)
(let* ((track-data (org-music-parse-link path))
(file (org-music-find-track-file
(nth 0 track-data) (nth 1 track-data)))
(cover-img (org-music-cover-image file))
(newline-str (cond ((org-export-derived-backend-p backend 'html) "<br>")
((org-export-derived-backend-p backend 'latex) "\\newline ")
(t " ")))
(text (org-music-export-text path nil backend nil newline-str)))
(cond ((org-export-derived-backend-p backend 'html)
(format "<div class='music-track'>
<img src='%s'> <span>%s</span>
</div>" cover-img text)
)
((org-export-derived-backend-p backend 'latex)
(format "\\begin{tabular}{@{\\hspace{0.3\\columnwidth}}r@{\\hspace{0.1\\columnwidth}}p{0.4\\columnwidth}}
\\includegraphics[height=6em]{%s} & \\vspace{-0.12\\columnwidth}%s
\\end{tabular}" cover-img text))
(t text))))
)
#+END_SRC
***** YouTube
The ~[[yt:...]]~ links preview nicely, but don't export nicely. Thankfully, we can
fix that.
#+BEGIN_SRC emacs-lisp
(after! org
(org-link-set-parameters "yt" :export #'+org-export-yt)
(defun +org-export-yt (path desc backend _com)
(cond ((org-export-derived-backend-p backend 'html)
(format "<iframe width='440' \
height='335' \
src='https://www.youtube.com/embed/%s' \
frameborder='0' \
allowfullscreen>%s</iframe>" path (or "" desc)))
((org-export-derived-backend-p backend 'latex)
(format "\\href{https://youtu.be/%s}{%s}" path (or desc "youtube")))
(t (format "https://youtu.be/%s" path)))))
#+END_SRC
*** 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.
[[xkcd:1882]]
**** In editor
***** Font Display
Mixed pitch is great. As is ~+org-pretty-mode~, let's use them.
#+BEGIN_SRC emacs-lisp
(add-hook! 'org-mode-hook #'+org-pretty-mode #'mixed-pitch-mode)
#+END_SRC
2020-01-09 17:58:01 +00:00
Earlier I loaded the ~org-pretty-table~ package, let's enable it everywhere!
#+BEGIN_SRC emacs-lisp
(setq global-org-pretty-table-mode t)
#+END_SRC
2020-03-14 15:30:49 +00:00
Let's make headings a bit bigger
#+BEGIN_SRC emacs-lisp
(custom-set-faces!
2020-04-20 15:59:21 +00:00
'(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)
2020-03-14 15:30:49 +00:00
'(outline-8 :weight semi-bold)
'(outline-9 :weight semi-bold))
2020-06-29 18:56:06 +00:00
#+END_SRC
And the same with the title.
#+BEGIN_SRC emacs-lisp
(after! org
(custom-set-faces!
'(org-document-title :height 1.2)))
2020-03-14 15:30:49 +00:00
#+END_SRC
***** Symbols
2020-01-09 17:58:01 +00:00
It's also nice to change the character used for collapsed items (by default ~…~),
I think ~▾~ is better for indicating 'collapsed section'.
and add an extra ~org-bullet~ to the default list of four.
I've also added some fun alternatives, just commented out.
#+BEGIN_SRC emacs-lisp
;; (after! org
;; (use-package org-pretty-tags
;; :config
;; (setq org-pretty-tags-surrogate-strings
;; `(("uni" . ,(all-the-icons-faicon "graduation-cap" :face 'all-the-icons-purple :v-adjust 0.01))
;; ("ucc" . ,(all-the-icons-material "computer" :face 'all-the-icons-silver :v-adjust 0.01))
;; ("assignment" . ,(all-the-icons-material "library_books" :face 'all-the-icons-orange :v-adjust 0.01))
;; ("test" . ,(all-the-icons-material "timer" :face 'all-the-icons-red :v-adjust 0.01))
;; ("lecture" . ,(all-the-icons-fileicon "keynote" :face 'all-the-icons-orange :v-adjust 0.01))
;; ("email" . ,(all-the-icons-faicon "envelope" :face 'all-the-icons-blue :v-adjust 0.01))
;; ("read" . ,(all-the-icons-octicon "book" :face 'all-the-icons-lblue :v-adjust 0.01))
;; ("article" . ,(all-the-icons-octicon "file-text" :face 'all-the-icons-yellow :v-adjust 0.01))
;; ("web" . ,(all-the-icons-faicon "globe" :face 'all-the-icons-green :v-adjust 0.01))
;; ("info" . ,(all-the-icons-faicon "info-circle" :face 'all-the-icons-blue :v-adjust 0.01))
;; ("issue" . ,(all-the-icons-faicon "bug" :face 'all-the-icons-red :v-adjust 0.01))
;; ("someday" . ,(all-the-icons-faicon "calendar-o" :face 'all-the-icons-cyan :v-adjust 0.01))
;; ("idea" . ,(all-the-icons-octicon "light-bulb" :face 'all-the-icons-yellow :v-adjust 0.01))
;; ("emacs" . ,(all-the-icons-fileicon "emacs" :face 'all-the-icons-lpurple :v-adjust 0.01))))
;; (org-pretty-tags-global-mode)))
(after! org-superstar
(setq org-superstar-headline-bullets-list '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶")
;; org-superstar-headline-bullets-list '("" "Ⅱ" "Ⅲ" "Ⅳ" "" "Ⅵ" "Ⅶ" "Ⅷ" "Ⅸ" "")
org-superstar-prettify-item-bullets t ))
(after! org
(setq org-ellipsis " ▾ "
org-priority-highest ?A
2020-04-12 05:50:29 +00:00
org-priority-lowest ?E
2020-05-08 06:40:36 +00:00
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))))
2020-01-09 17:58:01 +00:00
#+END_SRC
It's also nice to make use of the Unicode characters for check boxes, and other commands.
2020-01-09 17:58:01 +00:00
#+BEGIN_SRC emacs-lisp
(after! org
2020-08-26 09:00:03 +00:00
(appendq! +ligatures-extra-symbols
`(:checkbox "☐"
:pending "◼"
:checkedbox "☑"
:list_property "∷"
:results "🠶"
:property "☸"
:properties "⚙"
:end "∎"
:options "⌥"
:title "𝙏"
:subtitle "𝙩"
:author "𝘼"
:date "𝘿"
:latex_header "⇥"
2020-04-28 13:09:04 +00:00
:latex_class "🄲"
:beamer_header "↠"
:begin_quote ""
:end_quote ""
:begin_export "⯮"
:end_export "⯬"
:priority_a ,(propertize "⚑" 'face 'all-the-icons-red)
:priority_b ,(propertize "⬆" 'face 'all-the-icons-orange)
:priority_c ,(propertize "■" 'face 'all-the-icons-yellow)
:priority_d ,(propertize "⬇" 'face 'all-the-icons-green)
:priority_e ,(propertize "❓" 'face 'all-the-icons-blue)
:em_dash "—"))
2020-08-26 09:00:03 +00:00
(set-ligatures! 'org-mode
2020-01-09 17:58:01 +00:00
:merge t
:checkbox "[ ]"
:pending "[-]"
:checkedbox "[X]"
:list_property "::"
:results "#+RESULTS:"
:property "#+PROPERTY:"
:property ":PROPERTIES:"
:end ":END:"
:options "#+OPTIONS:"
:title "#+TITLE:"
:subtitle "#+SUBTITLE:"
:author "#+AUTHOR:"
:date "#+DATE:"
2020-04-28 13:09:04 +00:00
:latex_class "#+LATEX_CLASS:"
:latex_header "#+LATEX_HEADER:"
:beamer_header "#+BEAMER_HEADER:"
:begin_quote "#+BEGIN_QUOTE"
:end_quote "#+END_QUOTE"
:begin_export "#+BEGIN_EXPORT"
:end_export "#+END_EXPORT"
:priority_a "[#A]"
:priority_b "[#B]"
:priority_c "[#C]"
:priority_d "[#D]"
:priority_e "[#E]"
:em_dash "---"))
2020-08-26 09:00:03 +00:00
(plist-put +ligatures-extra-symbols :name "⁍") ; or could be good?
2020-01-09 17:58:01 +00:00
#+END_SRC
2020-02-16 08:13:17 +00:00
We also like ~org-fragtog~, and that wants a hook.
#+BEGIN_SRC emacs-lisp
(add-hook 'org-mode-hook 'org-fragtog-mode)
#+END_SRC
2020-02-03 05:01:57 +00:00
***** LaTeX Fragments
First off, we want those fragments to look good.
#+BEGIN_SRC emacs-lisp
(after! org
(setq org-highlight-latex-and-related '(native script entities)))
#+END_SRC
2020-02-03 05:01:57 +00:00
It's nice to customise the look of LaTeX fragments so they fit better in the
text --- like this \(\sqrt{\beta^2+3}-\sum_{\phi=1}^\infty \frac{x^\phi-1}{\Gamma(a)}\). Let's start by adding a sans font.
2020-02-03 05:01:57 +00:00
#+BEGIN_SRC emacs-lisp
(setq org-format-latex-header "\\documentclass{article}
\\usepackage[usenames]{color}
\\usepackage[T1]{fontenc}
\\usepackage{mathtools}
\\usepackage{textcomp,amssymb}
2020-02-26 11:05:41 +00:00
\\usepackage[makeroom]{cancel}
\\usepackage{booktabs}
2020-02-03 05:01:57 +00:00
\\pagestyle{empty} % do not remove
% The settings below are copied from fullpage.sty
\\setlength{\\textwidth}{\\paperwidth}
\\addtolength{\\textwidth}{-3cm}
\\setlength{\\oddsidemargin}{1.5cm}
\\addtolength{\\oddsidemargin}{-2.54cm}
\\setlength{\\evensidemargin}{\\oddsidemargin}
\\setlength{\\textheight}{\\paperheight}
\\addtolength{\\textheight}{-\\headheight}
\\addtolength{\\textheight}{-\\headsep}
\\addtolength{\\textheight}{-\\footskip}
\\addtolength{\\textheight}{-3cm}
\\setlength{\\topmargin}{1.5cm}
\\addtolength{\\topmargin}{-2.54cm}
% my custom stuff
2020-02-09 00:36:59 +00:00
\\usepackage{arev}
2020-02-03 05:01:57 +00:00
\\usepackage{arevmath}")
#+END_SRC
We can either render from a ~dvi~ or ~pdf~ file, so let's benchmark ~latex~ and
~pdflatex~.
| ~latex~ time | ~pdflatex~ time |
|------------+---------------|
| 135±2 ms | 215±3 ms |
On the rendering side, there are two ~.dvi~-to-image converters which I am
interested in: ~dvipng~ and ~dvisvgm~. Then with the a ~.pdf~ we have ~pdf2svg~.
For inline preview we care about speed, while for exporting we care about file
size and prefer a vector graphic.
Using the above latex expression and benchmarking lead to the following results:
| ~dvipng~ time | ~dvisvgm~ time | ~pdf2svg~ time |
|-------------+--------------+--------------|
| 89±2 ms | 178±2 ms | 12±2 ms |
Now let's combine this to see what's best
| Tool chain | Total time | Resultant file size |
|--------------------+------------+---------------------|
| ~latex~ + ~dvipng~ | 226±2 ms | 7 KiB |
| ~latex~ + ~dvisvgm~ | 392±4 ms | 8 KiB |
| ~pdflatex~ + ~pdf2svg~ | 230±2 ms | 16 KiB |
So, let's use ~dvipng~ for previewing LaTeX fragments in-Emacs, but ~dvisvgm~ for [[
2020-02-03 05:01:57 +00:00
Exporting to HTML][LaTeX Rendering]].
/Unfortunately: it seems that svg sizing is annoying ATM, so let's actually not do this right now./
2020-02-03 05:01:57 +00:00
As well as having a sans font, there are a few other tweaks which can make them
look better. Namely making sure that the colours switch when the theme does.
2020-02-03 05:01:57 +00:00
#+BEGIN_SRC emacs-lisp
(after! org
;; make background of fragments transparent
;; (let ((dvipng--plist (alist-get 'dvipng org-preview-latex-process-alist)))
;; (plist-put dvipng--plist :use-xcolor t)
;; (plist-put dvipng--plist :image-converter '("dvipng -D %D -bg 'transparent' -T tight -o %O %f")))
2020-02-09 15:14:21 +00:00
(add-hook! 'doom-load-theme-hook
(defun +org-refresh-latex-background ()
(plist-put! org-format-latex-options
:background
(face-attribute (or (cadr (assq 'default face-remapping-alist))
'default)
:background nil t))))
2020-02-03 05:01:57 +00:00
)
#+END_SRC
It'd be nice to make ~mhchem~ equations able to be rendered.
NB: This doesn't work at the moment.
#+BEGIN_SRC emacs-lisp
(after! org
(add-to-list 'org-latex-regexps '("\\ce" "^\\\\ce{\\(?:[^\000{}]\\|{[^\000}]+?}\\)}" 0 nil)))
#+END_SRC
***** Stolen from [[https://github.com/jkitchin/scimax][scimax]] (semi-working right now)
I want fragment justification
#+BEGIN_SRC emacs-lisp
(after! org
(defun scimax-org-latex-fragment-justify (justification)
"Justify the latex fragment at point with JUSTIFICATION.
JUSTIFICATION is a symbol for 'left, 'center or 'right."
(interactive
(list (intern-soft
(completing-read "Justification (left): " '(left center right)
nil t nil nil 'left))))
(let* ((ov (ov-at))
(beg (ov-beg ov))
(end (ov-end ov))
(shift (- beg (line-beginning-position)))
(img (overlay-get ov 'display))
(img (and (and img (consp img) (eq (car img) 'image)
(image-type-available-p (plist-get (cdr img) :type)))
img))
space-left offset)
(when (and img
;; This means the equation is at the start of the line
(= beg (line-beginning-position))
(or
(string= "" (s-trim (buffer-substring end (line-end-position))))
(eq 'latex-environment (car (org-element-context)))))
(setq space-left (- (window-max-chars-per-line) (car (image-size img)))
offset (floor (cond
((eq justification 'center)
(- (/ space-left 2) shift))
((eq justification 'right)
(- space-left shift))
(t
0))))
(when (>= offset 0)
(overlay-put ov 'before-string (make-string offset ?\ ))))))
(defun scimax-org-latex-fragment-justify-advice (beg end image imagetype)
"After advice function to justify fragments."
(scimax-org-latex-fragment-justify (or (plist-get org-format-latex-options :justify) 'left)))
(defun scimax-toggle-latex-fragment-justification ()
"Toggle if LaTeX fragment justification options can be used."
(interactive)
(if (not (get 'scimax-org-latex-fragment-justify-advice 'enabled))
(progn
(advice-add 'org--format-latex-make-overlay :after 'scimax-org-latex-fragment-justify-advice)
(put 'scimax-org-latex-fragment-justify-advice 'enabled t)
(message "Latex fragment justification enabled"))
(advice-remove 'org--format-latex-make-overlay 'scimax-org-latex-fragment-justify-advice)
(put 'scimax-org-latex-fragment-justify-advice 'enabled nil)
(message "Latex fragment justification disabled"))))
#+END_SRC
There's also this lovely equation numbering stuff I'll nick
#+BEGIN_SRC emacs-lisp
;; Numbered equations all have (1) as the number for fragments with vanilla
;; org-mode. This code injects the correct numbers into the previews so they
;; look good.
(after! org
(defun scimax-org-renumber-environment (orig-func &rest args)
"A function to inject numbers in LaTeX fragment previews."
(let ((results '())
(counter -1)
(numberp))
(setq results (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)))
(incf counter)
(cons begin counter))
((string-match "\\\\begin{align}" env)
(prog2
(incf counter)
(cons begin counter)
(with-temp-buffer
(insert env)
(goto-char (point-min))
;; \\ is used for a new line. Each one leads to a number
(incf counter (count-matches "\\\\$"))
;; unless there are nonumbers.
(goto-char (point-min))
(decf counter (count-matches "\\nonumber")))))
(t
(cons begin nil)))))
(when (setq numberp (cdr (assoc (point) results)))
(setf (car args)
(concat
(format "\\setcounter{equation}{%s}\n" numberp)
(car args)))))
(apply orig-func args))
(defun scimax-toggle-latex-equation-numbering ()
"Toggle whether LaTeX fragments are numbered."
(interactive)
(if (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.")))
(advice-add 'org-create-formula-image :around #'scimax-org-renumber-environment)
(put 'scimax-org-renumber-environment 'enabled t))
#+END_SRC
**** Exporting (general)
#+BEGIN_SRC emacs-lisp
(after! org (setq org-export-headline-levels 5)) ; I like nesting
#+END_SRC
I'm also going to make use of an item in =ox-extra= so that I can add an =:ignore:=
tag to headings for the content to be kept, but the heading itself ignored
(unlike =:noexport:= which ignored both heading and content). This is useful when
I want to use headings to provide a structure for writing that doesn't appear in
the final documents.
#+BEGIN_SRC emacs-lisp
(after! org
(require 'ox-extra)
(ox-extras-activate '(ignore-headlines)))
#+END_SRC
**** Exporting to HTML
I want to tweak a whole bunch of things. While I'll want my tweaks almost all
the time, occasionally I may want to test how something turns out using a more
default config. With that in mind, a global minor mode seems like the most
appropriate architecture to use.
#+BEGIN_SRC emacs-lisp
(define-minor-mode org-fancy-html-export-mode
"Toggle my fabulous org export tweaks. While this mode itself does a little bit,
the vast majority of the change in behaviour comes from switch statements in:
- `org-html-template-fancier'
- `org-html--build-meta-info-extended'
- `org-html-src-block-collapsable'
- `org-html-block-collapsable'
- `org-html-table-wrapped'
- `org-html--format-toc-headline-colapseable'
- `org-html--toc-text-stripped-leaves'
- `org-export-html-headline-anchor'"
:global t
:init-value t
(if org-fancy-html-export-mode
(setq org-html-style-default org-html-style-fancy
org-html-checkbox-type 'html-span)
(setq org-html-style-default org-html-style-plain
org-html-checkbox-type 'html)))
#+END_SRC
***** Extra header content
We want to tack on a few more bits to the start of the body. Unfortunately, there
doesn't seem to be any nice variable or hook, so we'll just override the
relevant function.
This is done to allow me to add the date and author to the page header,
implement a CSS-only light/dark theme toggle, and a sprinkle of [[https://ogp.me/][Open Graph]]
metadata.
#+BEGIN_SRC emacs-lisp
(defadvice! org-html-template-fancier (orig-fn contents info)
"Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options. Adds a few extra things to the body
compared to the default implementation."
:around #'org-html-template
(if (not org-fancy-html-export-mode)
(funcall orig-fn contents info)
(concat
(when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
(let* ((xml-declaration (plist-get info :html-xml-declaration))
(decl (or (and (stringp xml-declaration) xml-declaration)
(cdr (assoc (plist-get info :html-extension)
xml-declaration))
(cdr (assoc "html" xml-declaration))
"")))
(when (not (or (not decl) (string= "" decl)))
(format "%s\n"
(format decl
(or (and org-html-coding-system
(fboundp 'coding-system-get)
(coding-system-get org-html-coding-system 'mime-charset))
"iso-8859-1"))))))
(org-html-doctype info)
"\n"
(concat "<html"
(cond ((org-html-xhtml-p info)
(format
" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
(plist-get info :language) (plist-get info :language)))
((org-html-html5-p info)
(format " lang=\"%s\"" (plist-get info :language))))
">\n")
"<head>\n"
(org-html--build-meta-info info)
(org-html--build-head info)
(org-html--build-mathjax-config info)
"</head>\n"
"<body>\n<input type='checkbox' id='theme-switch'><div id='page'><label id='switch-label' for='theme-switch'></label>"
(let ((link-up (org-trim (plist-get info :html-link-up)))
(link-home (org-trim (plist-get info :html-link-home))))
(unless (and (string= link-up "") (string= link-home ""))
(format (plist-get info :html-home/up-format)
(or link-up link-home)
(or link-home link-up))))
;; Preamble.
(org-html--build-pre/postamble 'preamble info)
;; Document contents.
(let ((div (assq 'content (plist-get info :html-divs))))
(format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div)))
;; Document title.
(when (plist-get info :with-title)
(let ((title (and (plist-get info :with-title)
(plist-get info :title)))
(subtitle (plist-get info :subtitle))
(html5-fancy (org-html--html5-fancy-p info)))
(when title
(format
"<div class='page-header'><div class='page-meta'>%s, %s</div><h1 class=\"title\">%s%s</h1></div>\n"
(format-time-string "%Y-%m-%d %A %-I:%M%p")
(org-export-data (plist-get info :author) info)
(org-export-data title info)
(if subtitle
(format
(if html5-fancy
"<p class=\"subtitle\">%s</p>\n"
(concat "\n" (org-html-close-tag "br" nil info) "\n"
"<span class=\"subtitle\">%s</span>\n"))
(org-export-data subtitle info))
"")))))
contents
(format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
;; Postamble.
(org-html--build-pre/postamble 'postamble info)
;; Possibly use the Klipse library live code blocks.
(when (plist-get info :html-klipsify-src)
(concat "<script>" (plist-get info :html-klipse-selection-script)
"</script><script src=\""
org-html-klipse-js
"\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
org-html-klipse-css "\"/>"))
;; Closing document.
"</div>\n</body>\n</html>")))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(defun org-html--build-meta-entry (label identity &optional content-format &rest content-formatters)
"Construct <meta> tag with LABEL=\"IDENTITY\" and content from CONTENT-FORMAT and CONTENT-FORMATTER."
(concat "<meta "
(format "%s=\"%s" label identity)
(when content-format
(concat "\" content=\""
(replace-regexp-in-string
"\"" "&quot;"
(org-html-encode-plain-text
(if content-formatters
(apply #'format content-format content-formatters)
content-format)))))
"\" />\n"))
(defadvice! org-html--build-meta-info-extended (info)
"Return meta tags for exported document, with more meta than usual.
INFO is a plist used as a communication channel."
:override #'org-html--build-meta-info
(let* ((protect-string
(lambda (str)
(replace-regexp-in-string
"\"" "&quot;" (org-html-encode-plain-text str))))
(title (org-export-data (plist-get info :title) info))
;; Set title to an invisible character instead of leaving it
;; empty, which is invalid.
(title (if (org-string-nw-p title) title "&lrm;"))
(subtitle (org-export-data (plist-get info :subtitle) info))
(author (and (plist-get info :with-author)
(let ((auth (plist-get info :author)))
;; Return raw Org syntax.
(and auth (org-element-interpret-data auth)))))
(description (plist-get info :description))
(keywords (plist-get info :keywords))
(charset (or (and org-html-coding-system
(fboundp 'coding-system-get)
(coding-system-get org-html-coding-system
'mime-charset))
"iso-8859-1")))
(concat
(when (plist-get info :time-stamp-file)
(format-time-string
(concat "<!-- "
(plist-get info :html-metadata-timestamp-format)
" -->\n")))
(org-html--build-meta-entry "charset" charset)
(let ((viewport-options
(cl-remove-if-not (lambda (cell) (org-string-nw-p (cadr cell)))
(plist-get info :html-viewport))))
(if viewport-options
(org-html--build-meta-entry "name" "viewport"
(mapconcat
(lambda (elm) (format "%s=%s" (car elm) (cadr elm)))
viewport-options ", "))))
(format "<title>%s</title>\n" title)
(org-html--build-meta-entry "name" "generator" "Org Mode")
(when (org-string-nw-p author)
(org-html--build-meta-entry "name" "author" author))
(when (org-string-nw-p description)
(org-html--build-meta-entry "name" "description" description))
(when (org-string-nw-p keywords)
(org-html--build-meta-entry "name" "keywords" keywords))
(when org-fancy-html-export-mode
(concat
(org-html--build-meta-entry "name" "theme-color" "#77aa99")
(org-html--build-meta-entry "property" "og:title" title)
(org-html--build-meta-entry "property" "og:type" "article")
(org-html--build-meta-entry "property" "og:image" "https://tecosaur.com/resources/org/nib.png")
(when (org-string-nw-p author)
(org-html--build-meta-entry "property" "og:article:author:first_name" (car (s-split " " author))))
(when (and (org-string-nw-p author) (s-contains-p " " author))
(org-html--build-meta-entry "property" "og:article:author:first_name" (cdr (s-split-up-to " " author 2))))
(org-html--build-meta-entry "property" "og:article:published_time" (format-time-string "%FT%T%z"))
(when (org-string-nw-p subtitle)
(org-html--build-meta-entry "property" "og:description" subtitle)))))))
#+END_SRC
***** Custom CSS/JS
The default org HTML export is ... alright, but we can really jazz it up.
[[https://lepisma.xyz][lepisma.xyz]] has a really nice style, and from and org export too!
Suffice to say I've snatched it, with a few of my own tweaks applied.
#+BEGIN_SRC html :tangle misc/org-export-header.html :comments no
<link rel="icon" href="https://tecosaur.com/resources/org/nib.ico" type="image/ico" />
2020-08-29 15:32:00 +00:00
<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-roman-webfont.woff2">
<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-italic-webfont.woff2">
<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextRegular.woff2">
<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextItalic.woff2">
<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextBold.woff2">
#+END_SRC
#+BEGIN_SRC emacs-lisp
2020-05-23 19:17:49 +00:00
(after! org
(setq org-html-style-fancy
(concat (f-read-text (expand-file-name "misc/org-export-header.html" doom-private-dir))
"<script>\n"
(f-read-text (expand-file-name "misc/pile-css-theme/main.js" doom-private-dir))
"</script>\n<style>\n"
(f-read-text (expand-file-name "misc/pile-css-theme/main.css" doom-private-dir))
2020-07-17 04:53:03 +00:00
"</style>")
org-html-style-plain org-html-style-default
org-html-style-default org-html-style-fancy
2020-07-17 10:09:18 +00:00
org-html-htmlize-output-type 'css
2020-07-19 08:24:39 +00:00
org-html-doctype "html5"
2020-07-17 10:09:18 +00:00
org-html-html5-fancy t))
#+END_SRC
***** Collapsable src and example blocks
By wrapping the ~<pre>~ element in a ~<details>~ block, we can obtain collapsable
blocks with no CSS, though we will toss a little in anyway to have this looking
somewhat spiffy.
We can take our modification a step further, and add a gutter on the side of the
Src block containing both an anchor referencing the current block, and a button
to copy the content of the block.
#+NAME: Src blocks
#+BEGIN_SRC emacs-lisp
(defadvice! org-html-src-block-collapsable (orig-fn src-block contents info)
"Wrap the usual <pre> block in a <details>"
:around #'org-html-src-block
(if (not org-fancy-html-export-mode)
(funcall orig-fn src-block contents info)
(let* ((properties (cadr src-block))
(lang (mode-name-to-lang-name
(plist-get properties :language)))
(name (plist-get properties :name))
(ref (org-export-get-reference src-block info)))
(format
"<details id='%s' class='code'%s><summary%s>%s</summary>
<div class='gutter'>
<a href='#%s'>#</a>
<button title='Copy to clipboard' onclick='copyPreToClipdord(this)'>⎘</button>\
</div>
%s
</details>"
ref
(if (member (org-export-read-attribute :attr_html src-block :collapsed)
'("y" "yes" "t" "true"))
"" " open")
(if name " class='named'" "")
(if (not name) (concat "<span class='lang'>" lang "</span>")
(format "<span class='name'>%s</span><span class='lang'>%s</span>" name lang))
ref
(if name
(replace-regexp-in-string (format "<pre\\( class=\"[^\"]+\"\\)? id=\"%s\">" ref) "<pre\\1>"
(funcall orig-fn src-block contents info))
(funcall orig-fn src-block contents info))))))
(defun mode-name-to-lang-name (mode)
(or (cadr (assoc mode
'(("asymptote" "Asymptote")
("awk" "Awk")
("C" "C")
("clojure" "Clojure")
("css" "CSS")
("D" "D")
("ditaa" "ditaa")
("dot" "Graphviz")
("calc" "Emacs Calc")
("emacs-lisp" "Emacs Lisp")
("fortran" "Fortran")
("gnuplot" "gnuplot")
("haskell" "Haskell")
("hledger" "hledger")
("java" "Java")
("js" "Javascript")
("latex" "LaTeX")
("ledger" "Ledger")
("lisp" "Lisp")
("lilypond" "Lilypond")
("lua" "Lua")
("matlab" "MATLAB")
("mscgen" "Mscgen")
("ocaml" "Objective Caml")
("octave" "Octave")
("org" "Org mode")
("oz" "OZ")
("plantuml" "Plantuml")
("processing" "Processing.js")
("python" "Python")
("R" "R")
("ruby" "Ruby")
("sass" "Sass")
("scheme" "Scheme")
("screen" "Gnu Screen")
("sed" "Sed")
("sh" "shell")
("sql" "SQL")
("sqlite" "SQLite")
("forth" "Forth")
("io" "IO")
("J" "J")
("makefile" "Makefile")
("maxima" "Maxima")
("perl" "Perl")
("picolisp" "Pico Lisp")
("scala" "Scala")
("shell" "Shell Script")
("ebnf2ps" "ebfn2ps")
("cpp" "C++")
("abc" "ABC")
("coq" "Coq")
("groovy" "Groovy")
("bash" "bash")
("csh" "csh")
("ash" "ash")
("dash" "dash")
("ksh" "ksh")
("mksh" "mksh")
("posh" "posh")
("ada" "Ada")
("asm" "Assembler")
("caml" "Caml")
("delphi" "Delphi")
("html" "HTML")
("idl" "IDL")
("mercury" "Mercury")
("metapost" "MetaPost")
("modula-2" "Modula-2")
("pascal" "Pascal")
("ps" "PostScript")
("prolog" "Prolog")
("simula" "Simula")
("tcl" "tcl")
("tex" "LaTeX")
("plain-tex" "TeX")
("verilog" "Verilog")
("vhdl" "VHDL")
("xml" "XML")
("nxml" "XML")
("conf" "Configuration File"))))
mode))
#+END_SRC
2020-07-17 18:25:27 +00:00
#+NAME: Example, fixed width, and property blocks
#+BEGIN_SRC emacs-lisp
(after! org
(defun org-html-block-collapsable (orig-fn block contents info)
"Wrap the usual block in a <details>"
(if (not org-fancy-html-export-mode)
(funcall orig-fn block contents info)
(let ((ref (org-export-get-reference block info))
(type (case (car block)
('property-drawer "Properties")))
(collapsed-default (case (car block)
('property-drawer t)
(t nil)))
(collapsed-value (org-export-read-attribute :attr_html block :collapsed)))
(format
"<details id='%s' class='code'%s>
<summary%s>%s</summary>
<div class='gutter'>\
<a href='#%s'>#</a>
<button title='Copy to clipboard' onclick='copyPreToClipdord(this)'>⎘</button>\
</div>
%s\n
</details>"
ref
(if (or (and collapsed-value (member collapsed-value '("y" "yes" "t" "true")))
collapsed-default)
"" " open")
(if type " class='named'" "")
(if type (format "<span class='type'>%s</span>" type) "")
ref
(funcall orig-fn block contents info)))))
(advice-add 'org-html-example-block :around #'org-html-block-collapsable)
(advice-add 'org-html-fixed-width :around #'org-html-block-collapsable)
(advice-add 'org-html-property-drawer :around #'org-html-block-collapsable))
#+END_SRC
2020-07-16 21:08:30 +00:00
***** Handle table overflow
In order to accommodate wide tables ---particularly on mobile devices--- we want
to set a maximum width and scroll overflow. Unfortunately, this cannot be applied
directly to the ~table~ element, so we have to wrap it in a ~div~.
2020-07-19 09:57:31 +00:00
While we're at it, we can a link gutter, as we did with src blocks, and show the
~#+name~, if one is given.
2020-07-16 21:08:30 +00:00
#+BEGIN_SRC emacs-lisp
(defadvice! org-html-table-wrapped (orig-fn table contents info)
"Wrap the usual <table> in a <div>"
:around #'org-html-table
(if (not org-fancy-html-export-mode)
(funcall orig-fn table contents info)
(let* ((name (plist-get (cadr table) :name))
(ref (org-export-get-reference table info)))
(format "<div id='%s' class='table'>
2020-07-19 09:57:31 +00:00
<div class='gutter'><a href='#%s'>#</a></div>
<div class='tabular'>
%s
</div>\
</div>"
ref ref
(if name
(replace-regexp-in-string (format "<table id=\"%s\"" ref) "<table"
(funcall orig-fn table contents info))
(funcall orig-fn table contents info))))))
2020-07-16 21:08:30 +00:00
#+END_SRC
2020-07-17 15:20:09 +00:00
***** TOC as a collapsable tree
The TOC is much nicer to navigate as a collapsable tree. Unfortunately we cannot
achieve this with CSS alone. Thankfully we can avoid JS though, by adapting the
TOC generation code to use a ~label~ for each item, and a hidden ~checkbox~ to keep
track of state.
To add this, we need to change one line in [[file:~/.emacs.d/.local/straight/repos/org-mode/lisp/ox-html.el::(format "<a href=\"#%s\">%s</a>"][org-html--format-toc-headline]].
Since we can actually accomplish the desired effect by adding advice /around/ the
function, without overriding it --- let's do that to reduce the bug surface of
this config a tad.
#+BEGIN_SRC emacs-lisp
(defadvice! org-html--format-toc-headline-colapseable (orig-fn headline info)
"Add a label and checkbox to `org-html--format-toc-headline's usual output,
to allow the TOC to be a collapseable tree."
:around #'org-html--format-toc-headline
(if (not org-fancy-html-export-mode)
(funcall orig-fn headline info)
(let ((id (or (org-element-property :CUSTOM_ID headline)
(org-export-get-reference headline info))))
(format "<input type='checkbox' id='toc--%s'/><label for='toc--%s'>%s</label>"
id id (funcall orig-fn headline info)))))
2020-07-17 15:20:09 +00:00
#+END_SRC
Now, leaves (headings with no children) shouldn't have the ~label~ item. The
obvious way to achieve this is by including some /if no children.../ logic in
~org-html--format-toc-headline-colapseable~. Unfortunately, I can't my elisp isn't
up to par to extract the number of child headings from the mountain of info that
org provides.
#+BEGIN_SRC emacs-lisp
(defadvice! org-html--toc-text-stripped-leaves (orig-fn toc-entries)
"Remove label"
:around #'org-html--toc-text
(if (not org-fancy-html-export-mode)
(funcall orig-fn toc-entries)
(replace-regexp-in-string "<input [^>]+><label [^>]+>\\(.+?\\)</label></li>" "\\1</li>"
(funcall orig-fn toc-entries))))
2020-07-17 15:20:09 +00:00
#+END_SRC
***** Make verbatim different to code
Since we have =verbatim= and ~code~, let's use =verbatim= for key strokes.
#+BEGIN_SRC emacs-lisp
(setq org-html-text-markup-alist
'((bold . "<b>%s</b>")
(code . "<code>%s</code>")
(italic . "<i>%s</i>")
(strike-through . "<del>%s</del>")
(underline . "<span class=\"underline\">%s</span>")
(verbatim . "<kbd>%s</kbd>")))
#+END_SRC
***** Change checkbox type
We also want to use HTML checkboxes, however we want to get a bit fancier than default
#+BEGIN_SRC emacs-lisp
(after! org
(appendq! org-html-checkbox-types '((html-span .
((on . "<span class='checkbox'></span>")
(off . "<span class='checkbox'></span>")
(trans . "<span class='checkbox'></span>")))))
(setq org-html-checkbox-type 'html-span))
#+END_SRC
- [ ] I'm yet to do this
- [-] Work in progress
- [X] This is done
***** Header anchors
2020-07-19 16:33:28 +00:00
I want to add GitHub-style links on hover for headings.
#+BEGIN_SRC emacs-lisp
(after! org
(defun org-export-html-headline-anchor (text backend info)
(when (and (org-export-derived-backend-p backend 'html)
org-fancy-html-export-mode)
2020-05-23 19:17:49 +00:00
(unless org-msg-currently-exporting
(replace-regexp-in-string
"<h\\([0-9]\\) id=\"\\([a-z0-9-]+\\)\">\\(.*[^ ]\\)<\\/h[0-9]>" ; this is quite restrictive, but due to `org-heading-contraction' I can do this
"<h\\1 id=\"\\2\">\\3<a aria-hidden=\"true\" href=\"#\\2\">#</a> </h\\1>"
2020-05-23 19:17:49 +00:00
text))))
(add-to-list 'org-export-filter-headline-functions
'org-export-html-headline-anchor))
#+END_SRC
2020-05-23 19:17:49 +00:00
It's worth noting that ~org-msg-currently-exporting~ is defined in [[*Org Msg][Org Msg]].
2020-08-23 13:16:03 +00:00
***** Acronyms
I want to style acronyms nicely. For the sake of convenience in implementation
I've actually done this under the [[#org-latex-acronyms][LaTeX export section]], for the sake of
convenance in implementation (this transformation was first added there).
2020-02-03 05:01:57 +00:00
***** LaTeX Rendering
****** Pre-rendered
2020-05-23 04:17:08 +00:00
When displaying images, we want to resize by the reciprocal of ~preview-scale~.
Unfortunately that doesn't happen by default, but not to worry! Advice exists.
#+BEGIN_SRC emacs-lisp
(after! org
(defadvice! org-html-latex-fragment-scaled (latex-fragment _contents info)
"Transcode a LATEX-FRAGMENT object from Org to HTML.
2020-05-23 04:17:08 +00:00
CONTENTS is nil. INFO is a plist holding contextual information."
:override #'org-html-latex-fragment
(let ((latex-frag (org-element-property :value latex-fragment))
(processing-type (plist-get info :with-latex))
(attrs '(:class (concat "latex-fragment"
(if (equal "\\(" (substring latex-frag) 0 2)
"inline" "block")))))
(when (eq processing-type 'dvipng)
(plist-put attrs :style (format "transform: scale(%.3f)" (/ 1.0 preview-scale))))
(cond
((memq processing-type '(t mathjax))
(org-html-format-latex latex-frag 'mathjax info))
((memq processing-type '(t html))
(org-html-format-latex latex-frag 'html info))
((assq processing-type org-preview-latex-process-alist)
(let ((formula-link
(org-html-format-latex latex-frag processing-type info)))
(when (and formula-link (string-match "file:\\([^]]*\\)" formula-link))
(let ((source (org-export-file-uri (match-string 1 formula-link))))
(org-html--format-image source attrs info)))))
(t latex-frag))))
(defadvice! org-html-latex-environment-scaled (latex-environment _contents info)
"Transcode a LATEX-ENVIRONMENT element from Org to HTML.
2020-05-23 04:17:08 +00:00
CONTENTS is nil. INFO is a plist holding contextual information."
:override #'org-html-latex-environment
(let ((processing-type (plist-get info :with-latex))
(latex-frag (org-remove-indentation
(org-element-property :value latex-environment)))
(attributes (org-export-read-attribute :attr_html latex-environment))
(label (and (org-element-property :name latex-environment)
(org-export-get-reference latex-environment info)))
(caption (and (org-html--latex-environment-numbered-p latex-environment)
(number-to-string
(org-export-get-ordinal
latex-environment info nil
(lambda (l _)
(and (org-html--math-environment-p l)
(org-html--latex-environment-numbered-p l))))))))
(plist-put attributes :class "latex-environment")
(when (eq processing-type 'dvipng)
(plist-put attributes :style (format "transform: scale(%.3f)" (/ 1.0 preview-scale))))
(cond
((memq processing-type '(t mathjax))
(org-html-format-latex
(if (org-string-nw-p label)
(replace-regexp-in-string "\\`.*"
(format "\\&\n\\\\label{%s}" label)
latex-frag)
latex-frag)
'mathjax info))
((assq processing-type org-preview-latex-process-alist)
(let ((formula-link
(org-html-format-latex
(org-html--unlabel-latex-environment latex-frag)
processing-type info)))
(when (and formula-link (string-match "file:\\([^]]*\\)" formula-link))
(let ((source (org-export-file-uri (match-string 1 formula-link))))
(org-html--wrap-latex-environment
(org-html--format-image source attributes info)
info caption label)))))
(t (org-html--wrap-latex-environment latex-frag info caption label))))))
2020-05-23 04:17:08 +00:00
#+END_SRC
2020-02-03 05:01:57 +00:00
On the maths side of things, I consider ~dvisvgm~ to be a rather compelling
option. However this isn't sized very well at the moment.
#+BEGIN_SRC emacs-lisp
;; (setq-default org-html-with-latex `dvisvgm)
#+END_SRC
2020-05-23 04:17:08 +00:00
****** MathJax
If MathJax is used, we want to use version 3 instead of the default version 2.
Looking at a [[https://www.intmath.com/cg5/katex-mathjax-comparison.php][comparison]] we seem to find that it is ~5 times as fast, uses a
single file instead of multiple, but seems to be a bit bigger unfortunately.
Thankfully this can be mitigated my adding the ~async~ attribute to defer loading.
#+BEGIN_SRC emacs-lisp
(after! org
(setq org-html-mathjax-options
'((path "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js" )
(scale "1")
(autonumber "ams")
(multlinewidth "85%")
(tagindent ".8em")
(tagside "right")))
(setq org-html-mathjax-template
"<script>
MathJax = {
chtml: {
scale: %SCALE
},
svg: {
scale: %SCALE,
fontCache: \"global\"
},
tex: {
tags: \"%AUTONUMBER\",
multlineWidth: \"%MULTLINEWIDTH\",
tagSide: \"%TAGSIDE\",
tagIndent: \"%TAGINDENT\"
}
};
</script>
2020-08-29 15:32:00 +00:00
<script id=\"MathJax-script\" async
src=\"%PATH\"></script>"))
#+END_SRC
**** Exporting to LaTeX
2020-08-23 13:16:03 +00:00
***** Acronyms
:PROPERTIES:
:CUSTOM_ID: org-latex-acronyms
:END:
I like automatically using spaced small caps for acronyms. For strings I want to
2020-07-17 18:25:27 +00:00
be unaffected let's use ~;~ as a prefix to prevent the transformation --- i.e.
~;JFK~ (as one would want for two-letter geographic locations and names).
2020-08-23 13:16:03 +00:00
While this is the LaTeX section, it's convenient to also provide HTML acronyms here.
#+BEGIN_SRC emacs-lisp
(after! org
(defun tec/org-export-latex-filter-acronym (text backend info)
2020-08-23 13:16:03 +00:00
(let ((the-backend
(cond
((org-export-derived-backend-p backend 'latex) 'latex)
((org-export-derived-backend-p backend 'html) 'html)))
(case-fold-search nil))
(when the-backend
(replace-regexp-in-string
"[;\\\\]?\\b[A-Z][A-Z]+s?"
(lambda (all-caps-str)
2020-08-23 13:16:03 +00:00
;; only format as acronym if str doesn't start with ";" or "\" (for LaTeX commands)
(cond ((equal (aref all-caps-str 0) ?\;) (substring all-caps-str 1))
((equal (aref all-caps-str 0) ?\\) all-caps-str)
((equal (aref all-caps-str (- (length all-caps-str) 1)) ?s)
2020-08-23 13:16:03 +00:00
(case the-backend
('latex
(concat "\\textls*[70]{\\textsc{" (s-downcase (substring all-caps-str 0 -1))
"}\\protect\\scalebox{.91}[.84]{s}}"))
('html
(concat "<span class='acr'>" (substring all-caps-str 0 -1)
2020-08-23 13:16:03 +00:00
"</span><small>s</small>"))))
(t (case the-backend
('latex
(concat "\\textls*[70]{\\textsc{" (s-downcase all-caps-str) "}}"))
('html (concat "<span class='acr'>" all-caps-str "</span>"))))))
text t t))))
(add-to-list 'org-export-filter-plain-text-functions
'tec/org-export-latex-filter-acronym)
;; FIXME I want to process headings, but this causes issues ATM,
;; specifically it passes (and formats) the entire section contents
;; (add-to-list 'org-export-filter-headline-functions
;; 'tec/org-export-latex-filter-acronym)
)
#+END_SRC
***** Nicer checkboxes
2020-05-19 12:29:44 +00:00
#+BEGIN_SRC emacs-lisp
(after! org
(defun tec/org-export-latex-fancy-item-checkboxes (text backend info)
(when (org-export-derived-backend-p backend 'latex)
(replace-regexp-in-string
"\\\\item\\[{$\\\\\\(\\w+\\)$}\\]"
(lambda (fullmatch)
(concat "\\\\item[" (pcase (substring fullmatch 9 -3) ; content of capture group
("square" "\\\\ifdefined\\\\checkboxUnchecked\\\\checkboxUnchecked\\\\else$\\\\square$\\\\fi" )
("boxminus" "\\\\ifdefined\\\\checkboxTransitive\\\\checkboxTransitive\\\\else$\\\\boxminus$\\\\fi")
("boxtimes" "\\\\ifdefined\\\\checkboxChecked\\\\checkboxChecked\\\\else$\\\\boxtimes$\\\\fi" )
(_ (substring fullmatch 9 -3))) "]"))
text)))
(add-to-list 'org-export-filter-item-functions
'tec/org-export-latex-fancy-item-checkboxes))
#+END_SRC
***** Class templates
#+BEGIN_SRC emacs-lisp :noweb yes
2020-03-23 03:01:56 +00:00
(after! ox-latex
(add-to-list 'org-latex-classes
'("fancy-article"
2020-05-19 12:29:44 +00:00
"\\documentclass{scrartcl}\n\
2020-03-23 03:01:56 +00:00
\\usepackage[T1]{fontenc}\n\
\\usepackage[osf,largesc,helvratio=0.9]{newpxtext}\n\
\\usepackage[scale=0.92]{sourcecodepro}\n\
\\usepackage[varbb]{newpxmath}\n\
2020-05-19 12:29:44 +00:00
2020-03-23 03:01:56 +00:00
\\usepackage[activate={true,nocompatibility},final,tracking=true,kerning=true,spacing=true,factor=2000]{microtype}\n\
\\usepackage{xcolor}\n\
\\usepackage{booktabs}
2020-05-19 12:29:44 +00:00
\\usepackage{subcaption}
\\usepackage[hypcap=true]{caption}
\\setkomafont{caption}{\\sffamily\\small}
\\setkomafont{captionlabel}{\\upshape\\bfseries}
\\captionsetup{justification=raggedright,singlelinecheck=true}
\\setcapindent{0pt}
2020-05-19 12:29:44 +00:00
2020-03-23 03:01:56 +00:00
\\setlength{\\parskip}{\\baselineskip}\n\
2020-05-19 12:29:44 +00:00
\\setlength{\\parindent}{0pt}\n\
\\usepackage{pifont}
\\newcommand{\\checkboxUnchecked}{$\\square$}
\\newcommand{\\checkboxTransitive}{\\rlap{\\raisebox{0.0ex}{\\hspace{0.35ex}\\Large\\textbf -}}$\\square$}
\\newcommand{\\checkboxChecked}{\\rlap{\\raisebox{0.2ex}{\\hspace{0.35ex}\\scriptsize \\ding{56}}}$\\square$}
2020-06-29 18:53:06 +00:00
\\newenvironment{warning}
{\\begin{center}
\\begin{tabular}{rp{0.9\\textwidth}}
\\ding{82} & \\textbf{Warning} \\\\ &
}
{
\\end{tabular}
\\end{center}
}
2020-05-19 12:29:44 +00:00
"
("\\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]
[NO-PACKAGES]
[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
2020-02-29 11:09:06 +00:00
(add-to-list 'org-latex-classes
2020-02-26 11:05:41 +00:00
'("bmc-article"
"\\documentclass[article,code,maths]{bmc}
[NO-DEFAULT-PACKAGES]
[NO-PACKAGES]
[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
2020-02-29 11:09:06 +00:00
(add-to-list 'org-latex-classes
2020-02-26 11:05:41 +00:00
'("bmc"
"\\documentclass[code,maths]{bmc}
[NO-DEFAULT-PACKAGES]
[NO-PACKAGES]
[EXTRA]"
("\\chapter{%s}" . "\\chapter*{%s}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
2020-03-23 03:01:56 +00:00
(setq org-latex-default-class "fancy-article")
<<org-latex-smart-minted>>
2020-04-20 08:00:48 +00:00
(setq org-latex-listings 'minted
org-latex-minted-options
'())
(setq org-latex-tables-booktabs t)
2020-03-01 13:22:15 +00:00
(setq org-latex-hyperref-template "
2020-05-14 19:13:12 +00:00
\\colorlet{greenyblue}{blue!70!green}
\\colorlet{blueygreen}{blue!40!green}
\\providecolor{link}{named}{greenyblue}
\\providecolor{cite}{named}{blueygreen}
\\hypersetup{
2020-03-23 03:01:56 +00:00
pdfauthor={%a},
pdftitle={%t},
pdfkeywords={%k},
pdfsubject={%d},
pdfcreator={%c},
pdflang={%L},
breaklinks=true,
colorlinks=true,
linkcolor=,
urlcolor=link,
citecolor=cite\n}
2020-03-23 03:01:56 +00:00
\\urlstyle{same}\n")
2020-02-29 11:09:06 +00:00
(setq org-latex-pdf-process
'("latexmk -shell-escape -interaction=nonstopmode -f -pdf -output-directory=%o %f")))
#+END_SRC
***** A cleverer preamble
We also want a particular preamble when minted is used,
#+NAME: org-minted-preamble
#+BEGIN_SRC LaTeX
\\usepackage{minted}
\\usepackage[many]{tcolorbox}
\\setminted{
frame=none,
% framesep=2mm,
baselinestretch=1.2,
fontsize=\\footnotesize,
highlightcolor=white!95!black!80!blue,
linenos,
breakanywhere=true,
breakautoindent=true,
breaklines=true,
tabsize=4,
xleftmargin=3.5em,
autogobble=true,
obeytabs=true,
python3=true,
% texcomments=true,
framesep=2mm,
breakbefore=\\\\\.+,
breakafter=\\,
style=autumn,
breaksymbol=\\color{white!60!black}\\tiny\\ensuremath{\\hookrightarrow},
breakanywheresymbolpre=\\,\\footnotesize\\ensuremath{_{\\color{white!60!black}\\rfloor}},
breakbeforesymbolpre=\\,\\footnotesize\\ensuremath{_{\\color{white!60!black}\\rfloor}},
breakaftersymbolpre=\\,\\footnotesize\\ensuremath{_{\\color{white!60!black}\\rfloor}},
}
\\BeforeBeginEnvironment{minted}{
\\begin{tcolorbox}[
enhanced,
overlay={\\fill[white!90!black] (frame.south west) rectangle ([xshift=2.8em]frame.north west);},
colback=white!95!black,
colframe=white!95!black, % make frame colour same as background
breakable,% Allow white breaks
arc=0pt,outer arc=0pt,sharp corners, % sharp corners
boxsep=0pt,left=0pt,right=0pt,top=0pt,bottom=0pt % no margin/paddding
]
}
\\AfterEndEnvironment{minted}{\\end{tcolorbox}}
\\renewcommand\\theFancyVerbLine{\\color{black!60!white}\\arabic{FancyVerbLine}} % minted line numbering
#+END_SRC
We also always want some particular elements in the preamble, let's call this
the "universal preamble"
#+NAME: org-universal-preamble
#+BEGIN_SRC LaTeX
\\usepackage[main,include]{embedall}
\\IfFileExists{./\\jobname.org}{\\embedfile[desc=The original file]{\\jobname.org}}{}
#+END_SRC
Instead of just loading doing the 'minted stuff' all the time, we can try to be
a bit cleverer, and handle the "universal preamble" while we're at it.
#+NAME: org-latex-smart-minted
#+BEGIN_SRC emacs-lisp :tangle no :noweb yes
(after! org
(defadvice! org-latex-header-smart-minted (orig-fn tpl def-pkg pkg snippets-p &optional extra)
"Include minted config if src blocks are detected."
:around #'org-splice-latex-header
(let ((header (funcall orig-fn tpl def-pkg pkg snippets-p extra))
(src-p (when (save-excursion
(goto-char (point-min))
(search-forward-regexp "#\\+BEGIN_SRC\\|#\\+begin_src" nil t))
t)))
(if snippets-p header
(concat header
org-latex-universal-preamble
(when src-p org-latex-minted-preamble)))))
(defvar org-latex-minted-preamble "
<<org-minted-preamble>>
"
"Preamble to be inserted when minted is used.")
(defvar org-latex-universal-preamble "
<<org-universal-preamble>>
"
"Preamble to be included in every export."))
#+END_SRC
***** Chameleon --- aka. match theme
Once the idea of having the look of the LaTeX document produced match the
current Emacs theme, I was enraptured. The result is the pseudo-class ~chameleon~.
#+BEGIN_SRC emacs-lisp
(after! ox
(defvar ox-chameleon-base-class "fancy-article"
"The base class that chameleon builds on")
(defvar ox-chameleon--p nil
"Used to indicate whether the current export is trying to blend in. Set just before being accessed.")
;; (setf (alist-get :filter-latex-class
;; (org-export-backend-filters
;; (org-export-get-backend 'latex)))
;; 'ox-chameleon-latex-class-detector-filter)
;; (defun ox-chameleon-latex-class-detector-filter (info backend)
;; ""
;; (setq ox-chameleon--p (when (equal (plist-get info :latex-class)
;; "chameleon")
;; (plist-put info :latex-class ox-chameleon-base-class)
;; t)))
;; TODO make this less hacky. One ideas was as follows
;; (map-put (org-export-backend-filters (org-export-get-backend 'latex))
;; :filter-latex-class 'ox-chameleon-latex-class-detector-filter))
;; Never seemed to execute though
(defadvice! ox-chameleon-org-latex-detect (orig-fun info)
:around #'org-export-install-filters
(setq ox-chameleon--p (when (equal (plist-get info :latex-class)
"chameleon")
(plist-put info :latex-class ox-chameleon-base-class)
t))
(funcall orig-fun info))
(defadvice! ox-chameleon-org-latex-export (orig-fn info &optional template snippet?)
:around #'org-latex-make-preamble
(funcall orig-fn info)
(if (not ox-chameleon--p)
(funcall orig-fn info template snippet?)
(concat (funcall orig-fn info template snippet?)
(ox-chameleon-generate-colourings))))
(defun ox-chameleon-generate-colourings ()
(apply #'format
"%% make document follow Emacs theme
\\definecolor{bg}{HTML}{%s}
\\definecolor{fg}{HTML}{%s}
\\definecolor{red}{HTML}{%s}
\\definecolor{orange}{HTML}{%s}
\\definecolor{green}{HTML}{%s}
\\definecolor{teal}{HTML}{%s}
\\definecolor{yellow}{HTML}{%s}
\\definecolor{blue}{HTML}{%s}
\\definecolor{dark-blue}{HTML}{%s}
\\definecolor{magenta}{HTML}{%s}
\\definecolor{violet}{HTML}{%s}
\\definecolor{cyan}{HTML}{%s}
\\definecolor{dark-cyan}{HTML}{%s}
\\definecolor{level1}{HTML}{%s}
\\definecolor{level2}{HTML}{%s}
\\definecolor{level3}{HTML}{%s}
\\definecolor{level4}{HTML}{%s}
\\definecolor{level5}{HTML}{%s}
\\definecolor{level6}{HTML}{%s}
\\definecolor{level7}{HTML}{%s}
\\definecolor{level8}{HTML}{%s}
\\definecolor{link}{HTML}{%s}
\\definecolor{cite}{HTML}{%s}
\\definecolor{itemlabel}{HTML}{%s}
\\definecolor{code}{HTML}{%s}
\\definecolor{verbatim}{HTML}{%s}
\\pagecolor{bg}
\\color{fg}
\\addtokomafont{section}{\\color{level1}}
\\newkomafont{sectionprefix}{\\color{level1}}
\\addtokomafont{subsection}{\\color{level2}}
\\newkomafont{subsectionprefix}{\\color{level2}}
\\addtokomafont{subsubsection}{\\color{level3}}
\\newkomafont{subsubsectionprefix}{\\color{level3}}
\\addtokomafont{paragraph}{\\color{level4}}
\\newkomafont{paragraphprefix}{\\color{level4}}
\\addtokomafont{subparagraph}{\\color{level5}}
\\newkomafont{subparagraphprefix}{\\color{level5}}
\\renewcommand{\\labelitemi}{\\textcolor{itemlabel}{\\textbullet}}
\\renewcommand{\\labelitemii}{\\textcolor{itemlabel}{\\normalfont\\bfseries \\textendash}}
\\renewcommand{\\labelitemiii}{\\textcolor{itemlabel}{\\textasteriskcentered}}
\\renewcommand{\\labelitemiv}{\\textcolor{itemlabel}{\\textperiodcentered}}
\\renewcommand{\\labelenumi}{\\textcolor{itemlabel}{\\theenumi.}}
\\renewcommand{\\labelenumii}{\\textcolor{itemlabel}{(\\theenumii)}}
\\renewcommand{\\labelenumiii}{\\textcolor{itemlabel}{\\theenumiii.}}
\\renewcommand{\\labelenumiv}{\\textcolor{itemlabel}{\\theenumiv.}}
\\DeclareTextFontCommand{\\texttt}{\\color{code}\\ttfamily}
\\makeatletter
\\def\\verbatim@font{\\color{verbatim}\\normalfont\\ttfamily}
\\makeatother
%% end customisations
"
(mapcar (doom-rpartial #'substring 1)
(list
(face-attribute 'solaire-default-face :background)
(face-attribute 'default :foreground)
;;
(doom-color 'red)
(doom-color 'orange)
(doom-color 'green)
(doom-color 'teal)
(doom-color 'yellow)
(doom-color 'blue)
(doom-color 'dark-blue)
(doom-color 'magenta)
(doom-color 'violet)
(doom-color 'cyan)
(doom-color 'dark-cyan)
;;
(face-attribute 'outline-1 :foreground)
(face-attribute 'outline-2 :foreground)
(face-attribute 'outline-3 :foreground)
(face-attribute 'outline-4 :foreground)
(face-attribute 'outline-5 :foreground)
(face-attribute 'outline-6 :foreground)
(face-attribute 'outline-7 :foreground)
(face-attribute 'outline-8 :foreground)
;;
(face-attribute 'link :foreground)
(or (face-attribute 'org-ref-cite-face :foreground) (doom-color 'yellow))
(face-attribute 'org-list-dt :foreground)
(face-attribute 'org-code :foreground)
(face-attribute 'org-verbatim :foreground)
))))
)
#+END_SRC
***** Make verbatim different to code
Since have just gone to so much effort above let's make the most of it by making
=verbatim= use ~verb~ instead of ~protectedtexttt~ (default).
#+BEGIN_SRC emacs-lisp
(setq org-latex-text-markup-alist '((bold . "\\textbf{%s}")
(code . protectedtexttt)
(italic . "\\emph{%s}")
(strike-through . "\\sout{%s}")
(underline . "\\uline{%s}")
(verbatim . verb)))
#+END_SRC
2020-01-20 18:15:09 +00:00
**** Exporting to Beamer
It's nice to use a different theme
#+BEGIN_SRC emacs-lisp
(setq org-beamer-theme "[progressbar=foot]metropolis")
#+END_SRC
Then customise it a bit
#+BEGIN_SRC emacs-lisp
#+END_SRC
And I think that it's natural to divide a presentation into sections, e.g.
Introduction, Overview... so let's set bump up the headline level that becomes a
frame from ~1~ to ~2~.
#+BEGIN_SRC emacs-lisp
(setq org-beamer-frame-level 2)
2020-01-20 18:17:20 +00:00
#+END_SRC
**** Exporting to GFM
We just need to load ~ox-gfm~ for org-mode documents
#+BEGIN_SRC emacs-lisp
(eval-after-load "org"
'(require 'ox-gfm nil t))
#+END_SRC
2020-01-09 18:02:54 +00:00
*** Babel
2020-02-09 03:06:01 +00:00
Doom lazy-loads babel languages, with is lovely.
We need to tell babel to use python3. Who uses python2 anymore anyway? And why
2020-01-20 18:17:00 +00:00
doesn't ~python~ refer to the latest version!?
#+BEGIN_SRC emacs-lisp
(setq org-babel-python-command "python3")
#+END_SRC
We also like auto-completion here
#+BEGIN_SRC emacs-lisp
(defun tec-org-python ()
(if (eq major-mode 'python-mode)
(progn (anaconda-mode t)
(company-mode t)))
)
(add-hook 'org-src-mode-hook 'tec-org-python)
#+END_SRC
2020-01-09 18:03:18 +00:00
*** ESS
We don't want ~R~ evaluation to hang the editor, hence
#+BEGIN_SRC emacs-lisp
(setq ess-eval-visibly 'nowait)
#+END_SRC
2020-01-09 18:03:42 +00:00
Syntax highlighting is nice, so let's turn all of that on
#+BEGIN_SRC emacs-lisp
(setq ess-R-font-lock-keywords '((ess-R-fl-keyword:keywords . t)
(ess-R-fl-keyword:constants . t)
(ess-R-fl-keyword:modifiers . t)
(ess-R-fl-keyword:fun-defs . t)
(ess-R-fl-keyword:assign-ops . t)
(ess-R-fl-keyword:%op% . t)
(ess-fl-keyword:fun-calls . t)
(ess-fl-keyword:numbers . t)
(ess-fl-keyword:operators . t)
(ess-fl-keyword:delimiters . t)
(ess-fl-keyword:= . t)
(ess-R-fl-keyword:F&T . t)))
#+END_SRC
** LaTeX
[[xkcd:1301]]
2020-03-23 03:12:19 +00:00
*** To-be-implemented ideas
- Paste image from clipboard
+ Determine first folder in ~graphicspath~ if applicable
+ Ask for file name
+ Use ~xclip~ to save file to graphics folder, or current directory (whichever applies)
2020-04-29 10:33:15 +00:00
#+BEGIN_SRC shell :eval no :tangle no
2020-03-23 03:12:19 +00:00
command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; }
if
xclip -selection clipboard -target image/png -o >/dev/null 2>&1
then
xclip -selection clipboard -target image/png -o >$1 2>/dev/null
echo $1
else
echo "no image"
fi
#+END_SRC
+ Insert figure, with filled in details as a result (activate =yasnippet= with
filename as variable maybe?)
2020-04-09 18:06:28 +00:00
*** Compilation
#+BEGIN_SRC emacs-lisp
(setq TeX-save-query nil
TeX-show-compilation t
TeX-command-extra-options "-shell-escape")
(after! latex
(add-to-list 'TeX-command-list '("XeLaTeX" "%`xelatex%(mode)%' %t" TeX-run-TeX nil t)))
#+END_SRC
2020-06-02 05:12:55 +00:00
For viewing the PDF, I rather like the pdf-tools viewer. While auctex is trying
to be nice in recognising that I have some PDF viewing apps installed, I'd
rather not have it default to using them, so let's re-order the preferences.
#+BEGIN_SRC emacs-lisp
(setq +latex-viewers '(pdf-tools evince zathura okular skim sumatrapdf))
#+END_SRC
*** Snippet helpers
**** Template
For use in the new-file template, let's set out a nice preamble we may want to use.
#+NAME: latex-nice-preable
#+BEGIN_SRC latex :tangle no
\\usepackage[pdfa,unicode=true,hidelinks]{hyperref}
\\usepackage[dvipsnames,svgnames,table,hyperref]{xcolor}
\\renewcommand{\\UrlFont}{\\ttfamily\\small}
\\usepackage[a-2b]{pdfx} % why not be archival
\\usepackage[T1]{fontenc}
\\usepackage[osf,helvratio=0.9]{newpxtext} % pallatino
\\usepackage[scale=0.92]{sourcecodepro}
\\usepackage[varbb]{newpxmath}
\\usepackage{mathtools}
\\usepackage{amssymb}
\\usepackage[activate={true,nocompatibility},final,tracking=true,kerning=true,spacing=true,factor=2000]{microtype}
% microtype makes text look nicer
\\usepackage{graphicx} % include graphics
\\usepackage{grffile} % fix allowed graphicx filenames
\\usepackage{booktabs} % nice table rules
#+END_SRC
Then let's bind the content to a function, and define some nice helpers.
#+BEGIN_SRC emacs-lisp :noweb yes
(setq tec/yas-latex-template-preamble "
<<latex-nice-preable>>
")
(defun tec/yas-latex-get-class-choice ()
"Prompt user for LaTeX class choice"
(setq tec/yas-latex-class-choice (ivy-read "Select document class: " '("article" "scrartcl" "bmc") :def "bmc")))
(defun tec/yas-latex-preamble-if ()
"Based on class choice prompt for insertion of default preamble"
(if (equal tec/yas-latex-class-choice "bmc") 'nil
(eq (read-char-choice "Include default preamble? [Type y/n]" '(?y ?n)) ?y)))
#+END_SRC
**** Deliminators
#+BEGIN_SRC emacs-lisp
(after! tex
(defvar tec/tex-last-delim-char nil
"Last open delim expanded in a tex document")
(defvar tec/tex-delim-dot-second t
"When the `tec/tex-last-delim-char' is . a second charachter (this) is prompted for")
(defun tec/get-open-delim-char ()
"Exclusivly read next char to tec/tex-last-delim-char"
(setq tec/tex-delim-dot-second nil)
(setq tec/tex-last-delim-char (read-char-exclusive "Opening deliminator, recognises: 9 ( [ { < | ."))
(when (eql ?. tec/tex-last-delim-char)
(setq tec/tex-delim-dot-second (read-char-exclusive "Other deliminator, recognises: 0 9 ( ) [ ] { } < > |"))))
(defun tec/tex-open-delim-from-char (&optional open-char)
"Find the associated opening delim as string"
(unless open-char (setq open-char (if (eql ?. tec/tex-last-delim-char)
tec/tex-delim-dot-second
tec/tex-last-delim-char)))
(case open-char
(?\( "(")
(?9 "(")
(?\[ "[")
(?\{ "\\{")
(?< "<")
(?| (if tec/tex-delim-dot-second "." "|"))
(t ".")))
(defun tec/tex-close-delim-from-char (&optional open-char)
"Find the associated closing delim as string"
(if tec/tex-delim-dot-second
(case tec/tex-delim-dot-second
(?\) ")")
(?0 ")")
(?\] "]")
(?\} "\\}")
(?\> ">")
(?| "|")
(t "."))
(case (or open-char tec/tex-last-delim-char)
(?\( ")")
(?9 ")")
(?\[ "]")
(?\{ "\\}")
(?< ">")
(?\) ")")
(?0 ")")
(?\] "]")
(?\} "\\}")
(?\> ">")
(?| "|")
(t "."))))
(defun tec/tex-next-char-smart-close-delim (&optional open-char)
(and (bound-and-true-p smartparens-mode)
(eql (char-after) (case (or open-char tec/tex-last-delim-char)
(?\( ?\))
(?\[ ?\])
(?{ ?})
(?< ?>)))))
(defun tec/tex-delim-yas-expand (&optional open-char)
(yas-expand-snippet (yas-lookup-snippet "_deliminators" 'latex-mode) (point) (+ (point) (if (tec/tex-next-char-smart-close-delim open-char) 2 1)))))
#+END_SRC
2020-03-14 16:26:06 +00:00
*** Editor visuals
2020-03-23 03:06:55 +00:00
Once again, /all hail mixed pitch mode!/
#+BEGIN_SRC emacs-lisp
(add-hook 'LaTeX-mode-hook #'mixed-pitch-mode)
#+END_SRC
2020-03-14 16:26:06 +00:00
Let's enhance ~TeX-fold-math~ a bit
#+BEGIN_SRC emacs-lisp
2020-04-09 18:28:04 +00:00
(after! latex
(setcar (assoc "⋆" LaTeX-fold-math-spec-list) "★")) ;; make \star bigger
2020-03-14 16:26:06 +00:00
(setq TeX-fold-math-spec-list
2020-04-09 18:06:28 +00:00
`(;; missing/better symbols
2020-03-14 16:26:06 +00:00
("≤" ("le"))
("≥" ("ge"))
("≠" ("ne"))
;; conviniance shorts -- these don't work nicely ATM
;; ("" ("left"))
;; ("" ("right"))
2020-03-14 16:26:06 +00:00
;; private macros
("" ("RR"))
("" ("NN"))
("" ("ZZ"))
("" ("QQ"))
("" ("CC"))
("" ("PP"))
("" ("HH"))
("𝔼" ("EE"))
("𝑑" ("dd"))
2020-03-16 13:28:37 +00:00
;; known commands
2020-03-14 16:26:06 +00:00
("" ("phantom"))
2020-04-14 15:23:36 +00:00
(,(lambda (num den) (if (and (TeX-string-single-token-p num) (TeX-string-single-token-p den))
(concat num "" den)
(concat "❪" num "" den "❫"))) ("frac"))
2020-04-09 18:06:28 +00:00
(,(lambda (arg) (concat "√" (TeX-fold-parenthesize-as-neccesary arg))) ("sqrt"))
(,(lambda (arg) (concat "⭡" (TeX-fold-parenthesize-as-neccesary arg))) ("vec"))
2020-03-14 16:26:06 +00:00
("{1}" ("text"))
2020-03-16 13:28:37 +00:00
;; private commands
2020-03-14 16:26:06 +00:00
("|{1}|" ("abs"))
("‖{1}‖" ("norm"))
("⌊{1}⌋" ("floor"))
("⌈{1}⌉" ("ceil"))
("⌊{1}⌉" ("round"))
2020-03-30 05:43:51 +00:00
("𝑑{1}/𝑑{2}" ("dv"))
("∂{1}/∂{2}" ("pdv"))
;; fancification
("{1}" ("mathrm"))
2020-04-09 18:06:28 +00:00
(,(lambda (word) (string-offset-roman-chars 119743 word)) ("mathbf"))
(,(lambda (word) (string-offset-roman-chars 119951 word)) ("mathcal"))
(,(lambda (word) (string-offset-roman-chars 120003 word)) ("mathfrak"))
(,(lambda (word) (string-offset-roman-chars 120055 word)) ("mathbb"))
(,(lambda (word) (string-offset-roman-chars 120159 word)) ("mathsf"))
(,(lambda (word) (string-offset-roman-chars 120367 word)) ("mathtt"))
2020-03-16 13:28:37 +00:00
)
TeX-fold-macro-spec-list
'(
;; as the defaults
("[f]" ("footnote" "marginpar"))
("[c]" ("cite"))
("[l]" ("label"))
("[r]" ("ref" "pageref" "eqref"))
("[i]" ("index" "glossary"))
("..." ("dots"))
("{1}" ("emph" "textit" "textsl" "textmd" "textrm" "textsf" "texttt"
"textbf" "textsc" "textup"))
;; tweaked defaults
("©" ("copyright"))
("®" ("textregistered"))
("™" ("texttrademark"))
("[1]:||►" ("item"))
("❡❡ {1}" ("part" "part*"))
("❡ {1}" ("chapter" "chapter*"))
("§ {1}" ("section" "section*"))
("§§ {1}" ("subsection" "subsection*"))
("§§§ {1}" ("subsubsection" "subsubsection*"))
("¶ {1}" ("paragraph" "paragraph*"))
("¶¶ {1}" ("subparagraph" "subparagraph*"))
;; extra
("⬖ {1}" ("begin"))
("⬗ {1}" ("end"))
2020-03-14 16:26:06 +00:00
))
2020-03-30 05:43:51 +00:00
(defun string-offset-roman-chars (offset word)
"Shift the codepoint of each charachter in WORD by OFFSET with an extra -6 shift if the letter is lowercase"
(apply 'string
(mapcar (lambda (c) (+ (if (>= c 97) (- c 6) c) offset)) word)))
2020-04-09 18:06:28 +00:00
(defun TeX-fold-parenthesize-as-neccesary (tokens &optional suppress-left suppress-right)
"Add ❪ ❫ parenthesis as if multiple LaTeX tokens appear to be present"
2020-04-14 15:23:36 +00:00
(if (TeX-string-single-token-p tokens) tokens
2020-04-09 18:06:28 +00:00
(concat (if suppress-left "" "❪")
tokens
(if suppress-right "" "❫"))))
2020-04-14 15:23:36 +00:00
(defun TeX-string-single-token-p (teststring)
"Return t if TESTSTRING appears to be a single token, nil otherwise"
(if (string-match-p "^\\\\?\\w+$" teststring) t nil))
2020-03-14 16:26:06 +00:00
#+END_SRC
2020-03-15 16:19:17 +00:00
Some local keybindings to make life a bit easier
#+BEGIN_SRC emacs-lisp
(after! tex
(map!
:map LaTeX-mode-map
:ei [C-return] #'LaTeX-insert-item
;; normal stuff here
:localleader
:desc "View" "v" #'TeX-view)
2020-03-15 16:19:17 +00:00
(setq TeX-electric-math '("\\(" . "")))
#+END_SRC
2020-03-14 16:26:06 +00:00
Maths deliminators can be de-emphasised a bit
#+BEGIN_SRC emacs-lisp
;; Making \( \) less visible
(defface unimportant-latex-face
'((t
2020-03-15 16:19:17 +00:00
:inherit font-lock-comment-face :family "Overpass" :weight light))
2020-03-14 16:26:06 +00:00
"Face used to make \\(\\), \\[\\] less visible."
:group 'LaTeX-math)
(font-lock-add-keywords
'latex-mode
`((,(rx (and "\\" (any "()[]"))) 0 'unimportant-latex-face prepend))
'end)
(font-lock-add-keywords
'latex-mode
`((,"\\\\[[:word:]]+" 0 'font-lock-keyword-face prepend))
'end)
#+END_SRC
And enable shell escape for the preview
#+BEGIN_SRC emacs-lisp
(setq preview-LaTeX-command '("%`%l \"\\nonstopmode\\nofiles\
\\PassOptionsToPackage{" ("," . preview-required-option-list) "}{preview}\
\\AtBeginDocument{\\ifx\\ifPreview\\undefined"
preview-default-preamble "\\fi}\"%' \"\\detokenize{\" %t \"}\""))
#+END_SRC
2020-03-15 16:19:17 +00:00
*** CDLaTeX
The symbols and modifies are very nice by default, but could do with a bit of
fleshing out. Let's change the prefix to a key which is similarly rarely used,
but more convenient, like =;=.
2020-03-15 16:19:17 +00:00
#+BEGIN_SRC emacs-lisp
(after! cdlatex
(setq ;; cdlatex-math-symbol-prefix ?\; ;; doesn't work at the moment :(
cdlatex-math-symbol-alist
'( ;; adding missing functions to 3rd level symbols
(?_ ("\\downarrow" "" "\\inf"))
(?2 ("^2" "\\sqrt{?}" "" ))
(?3 ("^3" "\\sqrt[3]{?}" "" ))
(?^ ("\\uparrow" "" "\\sup"))
(?k ("\\kappa" "" "\\ker"))
(?m ("\\mu" "" "\\lim"))
(?c ("" "\\circ" "\\cos"))
(?d ("\\delta" "\\partial" "\\dim"))
(?D ("\\Delta" "\\nabla" "\\deg"))
;; no idea why \Phi isnt on 'F' in first place, \phi is on 'f'.
(?F ("\\Phi"))
;; now just conveniance
(?. ("\\cdot" "\\dots"))
(?: ("\\vdots" "\\ddots"))
(?* ("\\times" "\\star" "\\ast")))
cdlatex-math-modify-alist
'( ;; my own stuff
(?B "\\mathbb" nil t nil nil)
(?a "\\abs" nil t nil nil))))
2020-03-15 16:19:17 +00:00
#+END_SRC
*** SyncTeX
#+BEGIN_SRC emacs-lisp
(after! tex
(add-to-list 'TeX-view-program-list '("Evince" "evince %o"))
(add-to-list 'TeX-view-program-selection '(output-pdf "Evince")))
#+END_SRC
*** Fixes
In case of Emacs28,
#+BEGIN_SRC emacs-lisp
(when EMACS28+
(add-hook 'latex-mode-hook #'TeX-latex-mode))
2020-05-18 16:01:23 +00:00
#+END_SRC
** Python
Since I'm using =mypyls=, as suggested in [[file:~/.emacs.d/modules/lang/python/README.org::*Language Server Protocol Support][:lang python LSP support]] I'll tweak the
priority of =mypyls=
#+BEGIN_SRC emacs-lisp
(after! lsp-python-ms
(set-lsp-priority! 'mspyls 1))
#+END_SRC
2020-01-09 18:03:42 +00:00
** R
*** Editor Visuals
#+BEGIN_SRC emacs-lisp
(after! ess-r-mode
2020-08-26 09:00:03 +00:00
(appendq! +ligatures-extra-symbols
2020-02-03 05:03:39 +00:00
'(:assign "⟵"
:multiply "×"))
2020-08-26 09:00:03 +00:00
(set-ligatures! 'ess-r-mode
2020-01-09 18:03:42 +00:00
;; Functional
:def "function"
;; Types
:null "NULL"
:true "TRUE"
:false "FALSE"
:int "int"
:floar "float"
:bool "bool"
;; Flow
:not "!"
:and "&&" :or "||"
:for "for"
:in "%in%"
:return "return"
;; Other
2020-02-03 05:03:39 +00:00
:assign "<-"
:multiply "%*%"))
2020-01-09 18:03:42 +00:00
#+END_SRC
2020-02-03 05:04:00 +00:00
** hledger
~ledger-mode~ is great and all, but ~hledger~ seems to be more actively maintained.
For example, from 2018--2020, the most prolific contributor to ~ledger~ produced
31 commits. For ~hledger~ this statistic is 1800 commits. In addition, over the
last decade, ~ledger~ seems to have lost steam, while ~hledger~ seems as actively
developed as ever. From this basic comparison ~hledger~ looks to have a more
promising outlook. It also has a few extra niceties that ~ledger~ doesn't, but is
2020-02-17 08:16:56 +00:00
a little slower (~haskell~ vs. ~c++~).
2020-02-03 05:04:00 +00:00
Since this uses the same format, and ~ledger-mode~ is well integrated into emacs,
2020-02-17 08:16:56 +00:00
and produced by John Wiegley --- author of ~ledger~ and current Emacs maintainer
2020-02-03 05:04:00 +00:00
--- using this seems like a good idea. Thankfully we can, with a little modification.
#+BEGIN_SRC emacs-lisp
(setq ledger-mode-should-check-version nil
ledger-report-links-in-register nil
ledger-binary-path "hledger")
#+END_SRC
** Markdown
Let's use mixed pitch, because it's great
#+BEGIN_SRC emacs-lisp
(add-hook! (gfm-mode markdown-mode) #'mixed-pitch-mode)
#+END_SRC
Most of the time when I write markdown, it's going into some app/website which
will do it's own line wrapping, hence we /only/ want to use visual line wrapping. No hard stuff.
#+BEGIN_SRC emacs-lisp
(add-hook! (gfm-mode markdown-mode) #'visual-line-mode #'turn-off-auto-fill)
#+END_SRC
Since markdown is often seen as rendered HTML, let's try to somewhat mirror the
2020-07-19 16:33:28 +00:00
style or markdown renderers.
Most markdown renders seem to make the first three headings levels larger than
normal text, the first two much so. Then the fourth level tends to be the same
as body text, while the fifth and sixth are (increasingly) smaller, with the
sixth greyed out. Since the sixth level is so small, I'll turn up the boldness a notch.
#+BEGIN_SRC emacs-lisp
(custom-set-faces!
'(markdown-header-face-1 :height 1.25 :weight extra-bold :inherit markdown-header-face)
'(markdown-header-face-2 :height 1.15 :weight bold :inherit markdown-header-face)
'(markdown-header-face-3 :height 1.08 :weight bold :inherit markdown-header-face)
'(markdown-header-face-4 :height 1.00 :weight bold :inherit markdown-header-face)
'(markdown-header-face-5 :height 0.90 :weight bold :inherit markdown-header-face)
'(markdown-header-face-6 :height 0.75 :weight extra-bold :inherit markdown-header-face))
#+END_SRC
2020-02-03 05:04:00 +00:00
** Beancount
2020-02-14 17:25:59 +00:00
The [[https://bitbucket.org/blais/beancount/src/tip/editors/emacs/beancount.el][beancount package]] online has been put into ~./lisp~, we just need to load and
enable it for ~.beancount~ files.
2020-02-03 05:04:00 +00:00
#+BEGIN_SRC emacs-lisp
(use-package! beancount
2020-03-18 16:49:23 +00:00
:load-path "~/.config/doom/lisp"
:mode ("\\.beancount\\'" . beancount-mode)
:config
(setq beancount-electric-currency t)
2020-03-18 16:49:23 +00:00
(defun beancount-bal ()
"Run bean-report bal."
(interactive)
(let ((compilation-read-command nil))
(beancount--run "bean-report"
(file-relative-name buffer-file-name) "bal")))
2020-05-17 03:29:11 +00:00
(map! :map beancount-mode-map
:n "TAB" #'beancount-align-to-previous-number
:i "TAB" #'beancount-tab-dwim))
2020-02-03 05:04:00 +00:00
#+END_SRC
2020-06-29 18:40:54 +00:00
** Authinfo
I just like syntax highlighting. So, let's define a mode with some simple font
locking.
#+BEGIN_SRC emacs-lisp :tangle lisp/authinfo-colour-mode.el :comments no
;;; authinfo-mode.el -*- lexical-binding: t; -*-
(setq authinfo-colour-keywords
2020-06-29 18:40:54 +00:00
'(("^#.*" . font-lock-comment-face)
("^\\(machine\\)[ \t]+\\([^ \t\n]+\\)"
2020-06-29 18:40:54 +00:00
(1 font-lock-variable-name-face)
(2 font-lock-builtin-face))
("\\(login\\)[ \t]+\\([^ \t\n]+\\)"
(1 font-lock-comment-delimiter-face)
(2 font-lock-keyword-face))
("\\(password\\)[ \t]+\\([^ \t\n]+\\)"
(1 font-lock-comment-delimiter-face)
2020-06-29 18:40:54 +00:00
(2 font-lock-doc-face))
("\\(port\\)[ \t]+\\([^ \t\n]+\\)"
(1 font-lock-comment-delimiter-face)
(2 font-lock-type-face))
("\\([^ \t\n]+\\)[, \t]+\\([^ \t\n]+\\)"
(1 font-lock-constant-face)
(2 nil))))
2020-06-29 18:40:54 +00:00
(defun authinfo-colour--hide-passwords (start end)
"Just `authinfo--hide-passwords' with a different colour face overlay."
(save-excursion
(save-restriction
(narrow-to-region start end)
(goto-char start)
(while (re-search-forward "\\bpassword +\\([^\n\t ]+\\)"
nil t)
(let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
(overlay-put overlay 'display (propertize "****"
'face 'font-lock-doc-face))
(overlay-put overlay 'reveal-toggle-invisible
#'authinfo-colour--toggle-display))))))
(defun authinfo-colour--toggle-display (overlay hide)
"Just `authinfo--toggle-display' with a different colour face overlay."
(if hide
(overlay-put overlay 'display (propertize "****" 'face 'font-lock-doc-face))
(overlay-put overlay 'display nil)))
(defvar authinfo-hide-passwords t
"Whether to hide passwords in authinfo.")
(define-derived-mode authinfo-colour-mode fundamental-mode "Authinfo"
"Major mode for editing .authinfo files.
Like `fundamental-mode', just with colour and passoword hiding."
(font-lock-add-keywords nil authinfo-colour-keywords)
2020-06-29 18:40:54 +00:00
(setq-local comment-start "#")
(setq-local comment-end "")
(when authinfo-hide-passwords
(authinfo-colour--hide-passwords (point-min) (point-max))
(reveal-mode)))
2020-06-29 18:40:54 +00:00
(provide 'authinfo-colour-mode)
2020-06-29 18:40:54 +00:00
#+END_SRC
Now we just need to load it appropriately.
#+BEGIN_SRC emacs-lisp
(use-package! authinfo-colour-mode
:mode ("authinfo\\.gpg\\'" . authinfo-colour-mode)
:init (advice-add 'authinfo-mode :override #'authinfo-colour-mode))
2020-06-29 18:40:54 +00:00
#+END_SRC