7973 lines
329 KiB
Org Mode
7973 lines
329 KiB
Org Mode
#+title: Doom Emacs Configuration
|
||
#+subtitle: The Methods, Management, and Menagerie of Madness
|
||
#+author: tecosaur
|
||
#+date: {{{modification-time(%Y-%m-%d %H:%M, t)}}} ;{{{timezone}}}, {{{git-rev}}}
|
||
#+macro: timezone (eval (substring (shell-command-to-string "date +%Z") 0 -1))
|
||
#+macro: git-rev (eval (format "@@html:<a href=\"https://github.com/tecosaur/emacs-config/commit/%1$s\" style=\"text-decoration: none\"><code style=\"padding: 0; color: var(--text-light); font-size: inherit; opacity: 0.7\">%1$s</code></a>@@@@latex:\\href{https://github.com/tecosaur/emacs-config/commit/%1$s}{%1$s}@@" (substring (shell-command-to-string "git rev-parse --short HEAD") 0 -1)))
|
||
#+startup: fold
|
||
#+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'>
|
||
|
||
#+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;">
|
||
<img src="https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg"
|
||
class="invertible" alt="GitHub Octicon"
|
||
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
|
||
|
||
#+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. @@latex:\mbox{@@--- Donald Knuth@@latex:}@@
|
||
#+end_quote
|
||
|
||
* 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.
|
||
|
||
Separately, online I have seen the following statement enough times I think it's a catchphrase
|
||
#+begin_quote
|
||
Redditor 1: I just discovered this thing, isn't it cool. \\
|
||
Redditor 2: Oh, there's an Emacs mode for that.
|
||
#+end_quote
|
||
|
||
This was enough for me to install Emacs, but I soon learned there are [[https://github.com/remacs/remacs#why-emacs][far more
|
||
compelling reasons]] to keep using it.
|
||
|
||
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
|
||
settled by the day. This is both my config, and a cautionary tale (just replace
|
||
"Linux" with "Emacs" in the comic below).
|
||
|
||
[[xkcd:456]]
|
||
|
||
** Why Emacs?
|
||
|
||
Emacs is [[https://www.eigenbahn.com/2020/01/12/emacs-is-no-editor][not a text editor]], this is a common misnomer. It is far more apt to
|
||
describe Emacs as /a Lisp machine providing a generic user-centric text
|
||
manipulation environment/. That's quite a mouthful.
|
||
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
|
||
|
||
Emacs can be thought of as a platform within which various elements of your
|
||
workflow may settle, with the potential for rich integrations between them --- a
|
||
/life/ IDE if you will.
|
||
|
||
Today, many aspects of daily computer usage are split between different
|
||
applications which act like islands, but this often doesn't mirror how we
|
||
/actually use/ our computers. Emacs, if one goes down the rabbit hole, can give
|
||
users the power to bridge this gap.
|
||
|
||
#+name: emacs-platform
|
||
#+begin_src dot :cmd circo :file misc/emacs-platform.svg :exports none
|
||
digraph {
|
||
graph [bgcolor="transparent"];
|
||
node [shape="underline" penwidth="2" style="rounded,filled" fillcolor="#efefef" color="#c9c9c9" fontcolor="#000000" fontname="overpass"];
|
||
edge [arrowhead=none color="#aaaaaa" penwidth="1.2"]
|
||
// nodes
|
||
"Task Managment" [color="#2ec27e"]
|
||
"Email" [color="#1c71d8"]
|
||
"Office suite" [color="#813d9c"]
|
||
"Code editor" [color="#f5c211"]
|
||
"Git client" [color="#e66100"]
|
||
// "News feed" [color="#c01c28"]
|
||
// "Personal Knowledge Base" [color="#986a44"]
|
||
|
||
"Task Managment" -> "Email"
|
||
"Task Managment" -> "Office suite"
|
||
"Task Managment" -> "Code editor"
|
||
"Task Managment" -> "Git client"
|
||
// "Task Managment" -> "News feed"
|
||
// "Task Managment" -> "Personal Knowledge Base"
|
||
|
||
"Email" -> "Office suite"
|
||
"Email" -> "Code editor"
|
||
"Email" -> "Git client"
|
||
// "Email" -> "Personal Knowledge Base"
|
||
|
||
"Office suite" -> "Code editor"
|
||
"Office suite" -> "Git client"
|
||
// "Office suite" -> "News feed"
|
||
// "Office suite" -> "Personal Knowledge Base"
|
||
|
||
"Code editor" -> "Git client"
|
||
|
||
// "News feed" -> "Personal Knowledge Base"
|
||
}
|
||
#+end_src
|
||
|
||
#+caption: Some sample workflow integrations that can be used within Emacs
|
||
#+attr_html: :class invertible :alt Graph of possible Emacs task integrations :style max-width:min(24em,100%)
|
||
[[file:misc/emacs-platform.svg]]
|
||
|
||
*** Some notably unique features
|
||
+ Recursive editing
|
||
+ Completely introspectable, with pervasive docstrings
|
||
+ Mutable environment, which can be incrementally modified
|
||
+ Functionality without applications
|
||
+ Client-server separation 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
|
||
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
|
||
external setup (i.e. outside of this config).
|
||
+ 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
|
||
so, no matter how much criticism you include I'll appreciate it :)
|
||
|
||
[[xkcd:1513]]
|
||
|
||
*** 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.
|
||
Helvetica is a masterful design, but I like a bit more pizzazz now and then.
|
||
+ A few LSP servers. Take a look at [[file:init.el][init.el]] to see which modules have the ~+lsp~ flag.
|
||
+ 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
|
||
+ 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
|
||
|
||
** Current Issues
|
||
*** Magit push in daemon
|
||
Quite often trying to push to a remote in the Emacs daemon produces as error like this:
|
||
#+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
|
||
*** 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]].
|
||
|
||
* Rudimentary configuration
|
||
Make this file run (slightly) faster with lexical binding (see [[https://nullprogram.com/blog/2016/12/22/][this blog post]]
|
||
for more info).
|
||
#+begin_src emacs-lisp :comments no
|
||
;;; config.el -*- lexical-binding: t; -*-
|
||
#+end_src
|
||
|
||
#+begin_src shell :exports none :comments no :tangle-mode (identity #o755)
|
||
#!/bin/bash
|
||
#+end_src
|
||
** Personal Information
|
||
It's useful to have some basic personal information
|
||
#+begin_src emacs-lisp
|
||
(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.
|
||
|
||
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
|
||
make a difference, and I don't want to accidentaly purge this file (I have done
|
||
~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
|
||
|
||
(setq undo-limit 80000000 ; Raise undo-limit to 80Mb
|
||
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
|
||
|
||
(if (equal "Battery status not available"
|
||
(battery))
|
||
(display-battery-mode 1) ; On laptops it's nice to know how much power you have
|
||
(setq password-cache-expiry nil)) ; I can trust my desktops ... can't I? (no battery = desktop)
|
||
|
||
(global-subword-mode 1) ; Iterate through CamelCase words
|
||
#+end_src
|
||
*** Frame sizing
|
||
It's nice to control the size of new frames, when launching Emacs that can be
|
||
done with ~emacs -geometry 160x48~. After the font size adjustment during
|
||
initialisation this works out to be ~102x31~.
|
||
|
||
Thanks to hotkeys, it's easy for me to expand a frame to half/full-screen, so it
|
||
makes sense to be conservative with the sizing of new frames.
|
||
|
||
Then, for creating new frames within the same Emacs instance, we'll just set the
|
||
default to be something roughly 80% of that size.
|
||
|
||
#+begin_src emacs-lisp
|
||
(add-to-list 'default-frame-alist '(height . 24))
|
||
(add-to-list 'default-frame-alist '(width . 80))
|
||
#+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
|
||
(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)
|
||
(+ivy/switch-buffer))
|
||
#+end_src
|
||
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
|
||
*** 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)
|
||
#+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
|
||
*** Modules
|
||
:PROPERTIES:
|
||
:header-args:emacs-lisp: :tangle no
|
||
:END:
|
||
Doom has this lovely /modular configuration base/ that takes a lot of work out of
|
||
configuring Emacs. Each module (when enabled) can provide a list of packages to
|
||
install (on ~doom sync~) and configuration to be applied. The modules can also
|
||
have flags applied to tweak their behaviour.
|
||
|
||
#+name: init.el
|
||
#+attr_html: :collapsed t
|
||
#+begin_src emacs-lisp :tangle "init.el" :noweb no-export :comments none
|
||
;;; init.el -*- lexical-binding: t; -*-
|
||
|
||
;; This file controls what Doom modules are enabled and what order they load in.
|
||
;; Press 'K' on a module to view its documentation, and 'gd' to browse its directory.
|
||
|
||
(doom! :completion
|
||
<<doom-completion>>
|
||
|
||
:ui
|
||
<<doom-ui>>
|
||
|
||
:editor
|
||
<<doom-editor>>
|
||
|
||
:emacs
|
||
<<doom-emacs>>
|
||
|
||
:term
|
||
<<doom-term>>
|
||
|
||
:checkers
|
||
<<doom-checkers>>
|
||
|
||
:tools
|
||
<<doom-tools>>
|
||
|
||
:os
|
||
<<doom-os>>
|
||
|
||
:lang
|
||
<<doom-lang>>
|
||
|
||
:email
|
||
<<doom-email>>
|
||
|
||
:app
|
||
<<doom-app>>
|
||
|
||
:config
|
||
<<doom-config>>
|
||
)
|
||
#+end_src
|
||
|
||
**** Structure
|
||
As you may have noticed by this point, this is a [[https://en.wikipedia.org/wiki/Literate_programming][literate]] configuration. Doom
|
||
has good support for this which we access though the ~literate~ module.
|
||
|
||
While we're in the ~:config~ section, we'll use Dooms nicer defaults, along with
|
||
the bindings and smartparens behaviour (the flags aren't documented, but they exist).
|
||
#+name: doom-config
|
||
#+begin_src emacs-lisp
|
||
literate
|
||
(default +bindings +smartparens)
|
||
#+end_src
|
||
|
||
**** Interface
|
||
There's a lot that can be done to enhance Emacs' capabilities.
|
||
I reckon enabling half the modules Doom provides should do it.
|
||
|
||
#+name: doom-completion
|
||
#+begin_src emacs-lisp
|
||
(company ; the ultimate code completion backend
|
||
+childframe) ; ... when your children are better than you
|
||
;;helm ; the *other* search engine for love and life
|
||
;;ido ; the other *other* search engine...
|
||
(ivy ; a search engine for love and life
|
||
+icons ; ... icons are nice
|
||
+prescient) ; ... I know what I want(ed)
|
||
#+end_src
|
||
|
||
#+name: doom-ui
|
||
#+begin_src emacs-lisp
|
||
;;deft ; notational velocity for Emacs
|
||
doom ; what makes DOOM look the way it does
|
||
doom-dashboard ; a nifty splash screen for Emacs
|
||
doom-quit ; DOOM quit-message prompts when you quit Emacs
|
||
(emoji +unicode) ; 🙂
|
||
;;fill-column ; a `fill-column' indicator
|
||
hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
|
||
;;hydra ; quick documentation for related commands
|
||
;;indent-guides ; highlighted indent columns, notoriously slow
|
||
(ligatures +extra) ; ligatures and symbols to make your code pretty again
|
||
;;minimap ; show a map of the code on the side
|
||
modeline ; snazzy, Atom-inspired modeline, plus API
|
||
nav-flash ; blink the current line after jumping
|
||
;;neotree ; a project drawer, like NERDTree for vim
|
||
ophints ; highlight the region an operation acts on
|
||
(popup ; tame sudden yet inevitable temporary windows
|
||
+all ; catch all popups that start with an asterix
|
||
+defaults) ; default popup rules
|
||
;;(tabs ; an tab bar for Emacs
|
||
;; +centaur-tabs) ; ... with prettier tabs
|
||
treemacs ; a project drawer, like neotree but cooler
|
||
;;unicode ; extended unicode support for various languages
|
||
vc-gutter ; vcs diff in the fringe
|
||
vi-tilde-fringe ; fringe tildes to mark beyond EOB
|
||
(window-select +numbers) ; visually switch windows
|
||
workspaces ; tab emulation, persistence & separate workspaces
|
||
zen ; distraction-free coding or writing
|
||
#+end_src
|
||
|
||
#+name: doom-editor
|
||
#+begin_src emacs-lisp
|
||
(evil +everywhere) ; come to the dark side, we have cookies
|
||
file-templates ; auto-snippets for empty files
|
||
fold ; (nigh) universal code folding
|
||
(format) ; automated prettiness
|
||
;;god ; run Emacs commands without modifier keys
|
||
;;lispy ; vim for lisp, for people who don't like vim
|
||
multiple-cursors ; editing in many places at once
|
||
;;objed ; text object editing for the innocent
|
||
;;parinfer ; turn lisp into python, sort of
|
||
rotate-text ; cycle region at point between text candidates
|
||
snippets ; my elves. They type so I don't have to
|
||
;;word-wrap ; soft wrapping with language-aware indent
|
||
#+end_src
|
||
|
||
#+name: doom-emacs
|
||
#+begin_src emacs-lisp
|
||
(dired +icons) ; making dired pretty [functional]
|
||
electric ; smarter, keyword-based electric-indent
|
||
(ibuffer +icons) ; interactive buffer management
|
||
(undo +tree) ; persistent, smarter undo for your inevitable mistakes
|
||
vc ; version-control and Emacs, sitting in a tree
|
||
#+end_src
|
||
|
||
#+name: doom-term
|
||
#+begin_src emacs-lisp
|
||
;;eshell ; the elisp shell that works everywhere
|
||
;;shell ; simple shell REPL for Emacs
|
||
;;term ; basic terminal emulator for Emacs
|
||
vterm ; the best terminal emulation in Emacs
|
||
#+end_src
|
||
|
||
#+name: doom-checkers
|
||
#+begin_src emacs-lisp
|
||
syntax ; tasing you for every semicolon you forget
|
||
(:if (executable-find "aspell") spell) ; tasing you for misspelling mispelling
|
||
grammar ; tasing grammar mistake every you make
|
||
#+end_src
|
||
|
||
#+name: doom-tools
|
||
#+begin_src emacs-lisp
|
||
ansible ; a crucible for infrastructure as code
|
||
;;debugger ; FIXME stepping through code, to help you add bugs
|
||
;;direnv ; be direct about your environment
|
||
docker ; port everything to containers
|
||
;;editorconfig ; let someone else argue about tabs vs spaces
|
||
;;ein ; tame Jupyter notebooks with emacs
|
||
(eval +overlay) ; run code, run (also, repls)
|
||
;;gist ; interacting with github gists
|
||
(lookup ; helps you navigate your code and documentation
|
||
+dictionary ; dictionary/thesaurus is nice
|
||
+docsets) ; ...or in Dash docsets locally
|
||
lsp ; Language Server Protocol
|
||
;;macos ; MacOS-specific commands
|
||
(magit ; a git porcelain for Emacs
|
||
+forge) ; interface with git forges
|
||
make ; run make tasks from Emacs
|
||
;;pass ; password manager for nerds
|
||
pdf ; pdf enhancements
|
||
;;prodigy ; FIXME managing external services & code builders
|
||
rgb ; creating color strings
|
||
;;taskrunner ; taskrunner for all your projects
|
||
;;terraform ; infrastructure as code
|
||
;;tmux ; an API for interacting with tmux
|
||
upload ; map local to remote projects via ssh/ftp
|
||
#+end_src
|
||
|
||
#+name: doom-os
|
||
#+begin_src emacs-lisp
|
||
tty ; improve the terminal Emacs experience
|
||
#+end_src
|
||
|
||
**** Language support
|
||
We can be rather liberal with enabling support for languages as the associated
|
||
packages/configuration are (usually) only loaded when first opening an
|
||
associated file.
|
||
|
||
#+name: doom-lang
|
||
#+begin_src emacs-lisp
|
||
;;agda ; types of types of types of types...
|
||
;;cc ; C/C++/Obj-C madness
|
||
;;clojure ; java with a lisp
|
||
;;common-lisp ; if you've seen one lisp, you've seen them all
|
||
;;coq ; proofs-as-programs
|
||
;;crystal ; ruby at the speed of c
|
||
;;csharp ; unity, .NET, and mono shenanigans
|
||
data ; config/data formats
|
||
;;(dart +flutter) ; paint ui and not much else
|
||
;;elixir ; erlang done right
|
||
;;elm ; care for a cup of TEA?
|
||
emacs-lisp ; drown in parentheses
|
||
;;erlang ; an elegant language for a more civilized age
|
||
ess ; emacs speaks statistics
|
||
;;faust ; dsp, but you get to keep your soul
|
||
;;fsharp ; ML stands for Microsoft's Language
|
||
;;fstar ; (dependent) types and (monadic) effects and Z3
|
||
;;(go +lsp) ; the hipster dialect
|
||
;; (haskell +lsp) ; a language that's lazier than I am
|
||
;;hy ; readability of scheme w/ speed of python
|
||
;;idris ;
|
||
json ; At least it ain't XML
|
||
;;(java +meghanada) ; the poster child for carpal tunnel syndrome
|
||
(javascript +lsp) ; all(hope(abandon(ye(who(enter(here))))))
|
||
;;julia ; a better, faster MATLAB
|
||
;;kotlin ; a better, slicker Java(Script)
|
||
(latex ; writing papers in Emacs has never been so fun
|
||
+latexmk ; what else would you use?
|
||
+cdlatex ; quick maths symbols +fold) ; fold the clutter away nicities
|
||
;;lean ; proof that mathematicians need help
|
||
;;factor ; for when scripts are stacked against you
|
||
;;ledger ; an accounting system in Emacs
|
||
lua ; one-based indices? one-based indices
|
||
markdown ; writing docs for people to ignore
|
||
;;nim ; python + lisp at the speed of c
|
||
;;nix ; I hereby declare "nix geht mehr!"
|
||
;;ocaml ; an objective camel
|
||
(org ; organize your plain life in plain text
|
||
+pretty ; yessss my pretties! (nice unicode symbols)
|
||
+dragndrop ; drag & drop files/images into org buffers
|
||
;;+hugo ; use Emacs for hugo blogging
|
||
+jupyter ; ipython/jupyter support for babel
|
||
+pandoc ; export-with-pandoc support
|
||
+gnuplot ; who doesn't like pretty pictures
|
||
;;+pomodoro ; be fruitful with the tomato technique
|
||
+present ; using org-mode for presentations
|
||
+roam) ; wander around notes
|
||
;;perl ; write code no one else can comprehend
|
||
;;php ; perl's insecure younger brother
|
||
;;plantuml ; diagrams for confusing people more
|
||
;;purescript ; javascript, but functional
|
||
(python +lsp +pyright) ; beautiful is better than ugly
|
||
;;qt ; the 'cutest' gui framework ever
|
||
;;racket ; a DSL for DSLs
|
||
;;rest ; Emacs as a REST client
|
||
;;rst ; ReST in peace
|
||
;;(ruby +rails) ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
|
||
(rust +lsp) ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
|
||
;;scala ; java, but good
|
||
scheme ; a fully conniving family of lisps
|
||
sh ; she sells {ba,z,fi}sh shells on the C xor
|
||
;;sml ; no, the /other/ ML
|
||
;;solidity ; do you need a blockchain? No.
|
||
;;swift ; who asked for emoji variables?
|
||
;;terra ; Earth and Moon in alignment for performance.
|
||
web ; the tubes
|
||
yaml ; JSON, but readable
|
||
#+end_src
|
||
|
||
**** Everything in Emacs
|
||
It's just too convenient being able to have everything in Emacs.
|
||
I couldn't resist the Email and Feed modules.
|
||
|
||
#+name: doom-email
|
||
#+begin_src emacs-lisp
|
||
(:if (executable-find "mu") (mu4e +org +gmail))
|
||
;;notmuch
|
||
;;(wanderlust +gmail)
|
||
#+end_src
|
||
|
||
#+name: doom-app
|
||
#+begin_src emacs-lisp
|
||
;;calendar
|
||
everywhere ; *leave* Emacs!? You must be joking.
|
||
irc ; how neckbeards socialize
|
||
(rss +org) ; emacs as an RSS reader
|
||
;;twitter ; twitter client https://twitter.com/vnought
|
||
#+end_src
|
||
|
||
*** Visual Settings
|
||
**** Font Face
|
||
'Fira Code' is nice, and 'Overpass' makes for a nice sans companion. We just need to
|
||
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
|
||
(setq doom-font (font-spec :family "JetBrains Mono" :size 24)
|
||
doom-big-font (font-spec :family "JetBrains Mono" :size 36)
|
||
doom-variable-pitch-font (font-spec :family "Overpass" :size 24)
|
||
doom-serif-font (font-spec :family "IBM Plex Mono" :weight 'light))
|
||
#+end_src
|
||
|
||
#+attr_html: :class invertible :alt Screenshot of the fonts within Emacs.
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/font-face.png]]
|
||
|
||
**** Theme and modeline
|
||
~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)
|
||
(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 (and (memq (plist-get (coding-system-plist buffer-file-coding-system) :category)
|
||
'(coding-category-undecided coding-category-utf-8))
|
||
(not (memq (coding-system-eol-type buffer-file-coding-system) '(1 2))))
|
||
t)))
|
||
|
||
(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
|
||
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
|
||
#+begin_src emacs-lisp
|
||
(custom-set-faces! '(doom-modeline-evil-insert-state :weight bold :foreground "#339CDB"))
|
||
#+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
|
||
- ~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
|
||
*** 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
|
||
'(""
|
||
(:eval
|
||
(if (s-contains-p org-roam-directory (or buffer-file-name ""))
|
||
(replace-regexp-in-string
|
||
".*/[0-9]*-?" "☰ "
|
||
(subst-char-in-string ?_ ? buffer-file-name))
|
||
"%b"))
|
||
(:eval
|
||
(let ((project-name (projectile-project-name)))
|
||
(unless (string= "-" project-name)
|
||
(format (if (buffer-modified-p) " ◉ %s" " ● %s") project-name))))))
|
||
#+end_src
|
||
|
||
For example when I open my config file it the window will be titled =config.org ●
|
||
doom= then as soon as I make a change it will become =config.org ◉ doom=.
|
||
*** Splash screen
|
||
Emacs can render an image as the splash screen, and [[https://github.com/MarioRicalde][@MarioRicalde]] came up with a
|
||
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.
|
||
#+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")
|
||
theme-name
|
||
"-" (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)
|
||
"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)
|
||
(goto-char (point-min))
|
||
(while (re-search-forward (car substitution) nil t)
|
||
(replace-match (doom-color (cdr substitution)) nil nil)))
|
||
(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)
|
||
(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)))
|
||
|
||
(add-hook 'window-size-change-functions #'set-appropriate-splash)
|
||
(add-hook 'doom-load-theme-hook #'set-appropriate-splash)
|
||
#+end_src
|
||
|
||
#+attr_html: :class invertible :alt The splash screen, just loaded.
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/splash-screen.png]]
|
||
|
||
*** Systemd daemon
|
||
For running a systemd service for a Emacs server I have the following
|
||
#+name: emacsclient service
|
||
#+begin_src systemd :tangle ~/.config/systemd/user/emacs.service :mkdirp yes
|
||
[Unit]
|
||
Description=Emacs server daemon
|
||
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
|
||
|
||
[Service]
|
||
Type=forking
|
||
ExecStart=sh -c 'emacs --daemon && emacsclient -c --eval "(delete-frame)"'
|
||
ExecStop=/usr/bin/emacsclient --no-wait --eval "(progn (setq kill-emacs-hook nil) (kill emacs))"
|
||
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")
|
||
systemctl --user enable emacs.service
|
||
#+end_src
|
||
|
||
For some reason if a frame isn't opened early in the initialisation process, the
|
||
daemon doesn't seem to like opening frames later --- hence the ~&& emacsclient~
|
||
part of the =ExecStart= value.
|
||
|
||
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.
|
||
|
||
#+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="" --no-wait %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
|
||
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.
|
||
|
||
#+name: daemon initialisation
|
||
#+begin_src emacs-lisp
|
||
(defun greedily-do-daemon-setup ()
|
||
(require 'org)
|
||
(when (require 'mu4e nil t)
|
||
(setq mu4e-confirm-quit t)
|
||
(setq +mu4e-lock-greedy t)
|
||
(setq +mu4e-lock-relaxed t)
|
||
(+mu4e-lock-add-watcher)
|
||
(when (+mu4e-lock-available t)
|
||
(mu4e~start)))
|
||
(when (require 'elfeed nil t)
|
||
(run-at-time nil (* 8 60 60) #'elfeed-update)))
|
||
|
||
(when (daemonp)
|
||
(add-hook 'emacs-startup-hook #'greedily-do-daemon-setup))
|
||
#+end_src
|
||
*** Emacs client wrapper
|
||
I frequently want to make use of Emacs while in a terminal emulator. To make
|
||
this easier, I can construct a few handy aliases.
|
||
|
||
However, a little convenience script in =~/.local/bin= can have the same effect,
|
||
be available beyond the specific shell I plop the alias in, then also allow me
|
||
to add a few bells and whistles --- namely:
|
||
+ Accepting stdin by putting it in a temporary file and immediately opening it.
|
||
+ Guessing that the =tty= is a good idea when ~$DISPLAY~ is unset (relevant with SSH
|
||
sessions, among other things).
|
||
+ With a whiff of 24-bit color support, sets ~TERM~ variable to a =terminfo= that
|
||
(probably) announces 24-bit color support.
|
||
+ Changes GUI =emacsclient= instances to be non-blocking by default (~--no-wait~),
|
||
and instead take a flag to suppress this behaviour (~-w~).
|
||
|
||
I would use =sh=, but using arrays for argument manipulation is just too
|
||
convenient, so I'll raise the requirement to =bash=. Since arrays are the only
|
||
'extra' compared to =sh=, other shells like =csh= etc. should work too.
|
||
|
||
#+name: e
|
||
#+begin_src shell :tangle ~/.local/bin/e :mkdirp yes :tangle-mode (identity #o755) :comments none
|
||
#!/usr/bin/env bash
|
||
force_tty=false
|
||
force_wait=false
|
||
stdin_mode=""
|
||
|
||
args=()
|
||
|
||
while :; do
|
||
case "$1" in
|
||
-t | -nw | --tty)
|
||
force_tty=true
|
||
shift ;;
|
||
-w | --wait)
|
||
force_wait=true
|
||
shift ;;
|
||
-m | --mode)
|
||
stdin_mode=" ($2-mode)"
|
||
shift 2 ;;
|
||
-h | --help)
|
||
echo -e "\033[1mUsage: e [-t] [-m MODE] [OPTIONS] FILE [-]\033[0m
|
||
|
||
Emacs client convenience wrapper.
|
||
|
||
\033[1mOptions:\033[0m
|
||
\033[0;34m-h, --help\033[0m Show this message
|
||
\033[0;34m-t, -nw, --tty\033[0m Force terminal mode
|
||
\033[0;34m-w, --wait\033[0m Don't supply \033[0;34m--no-wait\033[0m to graphical emacsclient
|
||
\033[0;34m-\033[0m Take \033[0;33mstdin\033[0m (when last argument)
|
||
\033[0;34m-m MODE, --mode MODE\033[0m Mode to open \033[0;33mstdin\033[0m with
|
||
|
||
Run \033[0;32memacsclient --help\033[0m to see help for the emacsclient."
|
||
exit 0 ;;
|
||
--*=*)
|
||
set -- "$@" "${1%%=*}" "${1#*=}"
|
||
shift ;;
|
||
,*)
|
||
if [ "$#" = 0 ]; then
|
||
break; fi
|
||
args+=("$1")
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
if [ ! "${#args[*]}" = 0 ] && [ "${args[-1]}" = "-" ]; then
|
||
unset 'args[-1]'
|
||
TMP="$(mktemp /tmp/emacsstdin-XXX)"
|
||
cat > "$TMP"
|
||
args+=(--eval "(let ((b (generate-new-buffer \"*stdin*\"))) (switch-to-buffer b) (insert-file-contents \"$TMP\") (delete-file \"$TMP\")${stdin_mode})")
|
||
fi
|
||
|
||
if [ -z "$DISPLAY" ] || $force_tty; then
|
||
# detect terminals with sneaky 24-bit support
|
||
if { [ "$COLORTERM" = truecolor ] || [ "$COLORTERM" = 24bit ]; } \
|
||
&& [ "$(tput colors 2>/dev/null)" -lt 257 ]; then
|
||
if echo "$TERM" | grep -q "^\w\+-[0-9]"; then
|
||
termstub="${TERM%%-*}"; else
|
||
termstub="${TERM#*-}"; fi
|
||
if infocmp "$termstub-direct" >/dev/null 2>&1; then
|
||
TERM="$termstub-direct"; else
|
||
TERM="xterm-direct"; fi # should be fairly safe
|
||
fi
|
||
emacsclient --tty -create-frame --alternate-editor="" "${args[@]}"
|
||
else
|
||
if ! $force_wait; then
|
||
args+=(--no-wait); fi
|
||
emacsclient -create-frame --alternate-editor="" "${args[@]}"
|
||
fi
|
||
#+end_src
|
||
|
||
Now, to set an alias to use =e= with magit, and then for maximum laziness we can
|
||
set aliases for the terminal-forced variants.
|
||
#+begin_src shell :tangle no
|
||
alias m='e --eval "(progn (magit-status) (delete-other-windows))"'
|
||
alias mt="m -t"
|
||
alias et="e -t"
|
||
#+end_src
|
||
|
||
* Package loading
|
||
:PROPERTIES:
|
||
:header-args:emacs-lisp: :tangle "packages.el" :comments no
|
||
:END:
|
||
This file shouldn't be byte compiled.
|
||
#+begin_src emacs-lisp :tangle "packages.el" :comments no
|
||
;; -*- no-byte-compile: t; -*-
|
||
#+end_src
|
||
** Loading instructions
|
||
:PROPERTIES:
|
||
: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
|
||
|
||
*** 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
|
||
** General packages
|
||
*** Window management
|
||
#+begin_src emacs-lisp
|
||
(package! rotate :pin "4e9ac3ff800880bd9b705794ef0f7c99d72900a6")
|
||
#+end_src
|
||
*** Fun
|
||
Sometimes one just wants a little fun.
|
||
XKCD comics are fun.
|
||
#+begin_src emacs-lisp
|
||
(package! xkcd :pin "66e928706fd660cfdab204c98a347b49c4267bdf")
|
||
#+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 "1840de71f7414b7cd6ce425747c8e26a413233aa")
|
||
#+end_src
|
||
|
||
Hey, let's get the weather in here while we're at it.
|
||
Unfortunately this seems slightly unmaintained ([[https://github.com/bcbcarl/emacs-wttrin/pulls][few open bugfix PRs]]) so let's
|
||
roll our [[file:lisp/wttrin.el][own version]].
|
||
#+begin_src emacs-lisp
|
||
(package! wttrin :recipe (:local-repo "lisp/wttrin"))
|
||
#+end_src
|
||
|
||
Why not flash words on the screen. Why not --- hey, it could be fun.
|
||
#+begin_src emacs-lisp
|
||
(package! spray :pin "74d9dcfa2e8b38f96a43de9ab0eb13364300cb46")
|
||
#+end_src
|
||
|
||
With all our fancy Emacs themes, my terminal is missing out!
|
||
#+begin_src emacs-lisp
|
||
(package! theme-magic :pin "844c4311bd26ebafd4b6a1d72ddcc65d87f074e3")
|
||
#+end_src
|
||
|
||
What's even the point of using Emacs unless you're constantly telling everyone
|
||
about it?
|
||
#+begin_src emacs-lisp
|
||
(package! elcord :pin "01b26d1af2f33a7c7c5a1c24d8bfb6d40115a7b0")
|
||
#+end_src
|
||
|
||
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.
|
||
|
||
#+attr_html: :class invertible :alt Screenshot of Keycast-mode in action
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/keycast.png]]
|
||
|
||
#+begin_src emacs-lisp
|
||
(package! keycast :pin "a3a0798349adf3e33277091fa8dee63173b68edf")
|
||
#+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--update t)
|
||
(add-to-list 'global-mode-string '("" mode-line-keycast " ")))
|
||
(remove-hook 'pre-command-hook 'keycast--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
|
||
|
||
In a similar manner, [[https://gitlab.com/ambrevar/emacs-gif-screencast][gif-screencast]] may come in handy.
|
||
#+begin_src emacs-lisp
|
||
(package! gif-screencast :pin "1145e676b160e7b1e5756f5b0f30dd31de252e1f")
|
||
#+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)
|
||
(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
|
||
*** Features
|
||
**** CalcTeX
|
||
This is a nice extension to ~calc~
|
||
|
||
# TODO add calctex screenshot
|
||
|
||
#+begin_src emacs-lisp
|
||
(package! calctex :recipe (:host github :repo "johnbcoughlin/calctex"
|
||
:files ("*.el" "calctex/*.el" "calctex-contrib/*.el" "org-calctex/*.el" "vendor"))
|
||
:pin "7fa2673c64e259e04aef684ccf09ef85570c388b")
|
||
#+end_src
|
||
**** Emacs everywhere
|
||
#+begin_src emacs-lisp
|
||
(package! emacs-everywhere :recipe (:local-repo "lisp/emacs-everywhere") :pin nil)
|
||
#+end_src
|
||
**** ESS
|
||
View data frames better with
|
||
#+begin_src emacs-lisp
|
||
(package! ess-view :pin "d4e5a340b7bcc58c434867b97923094bd0680283")
|
||
#+end_src
|
||
**** 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
|
||
;; (package! magit-delta :recipe (:host github :repo "dandavison/magit-delta") :pin "fc4de96e3faa1c983728239c5e41cc9f074b73a2")
|
||
#+end_src
|
||
**** 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]]
|
||
|
||
#+begin_src emacs-lisp
|
||
(package! info-colors :pin "47ee73cc19b1049eef32c9f3e264ea7ef2aaf8a5")
|
||
#+end_src
|
||
**** 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" :disable t)
|
||
#+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.
|
||
**** Calibre and ebook reading
|
||
For managing my ebooks, I'll hook into the well-established ebook library
|
||
manager [[https://calibre-ebook.com/][calibre]]. A number of Emacs clients for this exist, but this seems like a
|
||
good option.
|
||
#+begin_src emacs-lisp
|
||
(package! calibredb :pin "1f38fc34a8c159846450d18b1ee50cc960349ee7")
|
||
#+end_src
|
||
|
||
Then for reading them, the only currently viable options seems to be [[https://depp.brause.cc/nov.el/][nov.el]].
|
||
#+begin_src emacs-lisp
|
||
(package! nov :pin "0ece7ccbf79c074a3e4fbad1d1fa06647093f8e4")
|
||
#+end_src
|
||
|
||
Together these should give me a rather good experience reading ebooks.
|
||
**** My mixed-pitch fork
|
||
I'd rather like to be able to use multiple faces with mixed-pitch, so I'll have
|
||
to use my own branch for now.
|
||
#+begin_src emacs-lisp
|
||
(package! mixed-pitch :recipe (:local-repo "lisp/mixed-pitch") :pin nil)
|
||
#+end_src
|
||
**** Screenshots
|
||
This makes it a breeze to take lovely screenshots.
|
||
#+begin_src emacs-lisp
|
||
(package! screenshot :recipe (:local-repo "lisp/screenshot"))
|
||
#+end_src
|
||
|
||
#+attr_html: :class invertible :alt Example screenshot.el screenshot
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/screenshot.png]]
|
||
|
||
Some light configuring is all we need, so we can make use of the [[https://github.com/Calinou/0x0][0x0]] wrapper
|
||
file uploading script (which I've renamed to ~upload~).
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! screenshot
|
||
:defer t
|
||
:config (setq screenshot-upload-fn "upload %s 2>/dev/null"))
|
||
#+end_src
|
||
** Language packages
|
||
*** LaTeX
|
||
For mathematical convenience, WIP
|
||
#+begin_src emacs-lisp
|
||
(package! aas :recipe (:host github :repo "ymarco/auto-activating-snippets")
|
||
:pin "5064c60408c3ab45693c5f516003141d56a57629")
|
||
(package! laas :recipe (:local-repo "lisp/LaTeX-auto-activating-snippets"))
|
||
#+end_src
|
||
And some basic config
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! aas
|
||
:commands aas-mode)
|
||
|
||
(use-package! laas
|
||
:hook (LaTeX-mode . laas-mode)
|
||
:config
|
||
(defun laas-tex-fold-maybe ()
|
||
(unless (equal "/" aas-transient-snippet-key)
|
||
(+latex-fold-last-macro-a)))
|
||
(add-hook 'aas-post-snippet-expand-hook #'laas-tex-fold-maybe))
|
||
#+end_src
|
||
*** Org Mode
|
||
Use HEAD for development.
|
||
#+begin_src emacs-lisp
|
||
(unpin! org-mode)
|
||
#+end_src
|
||
|
||
**** Improve agenda/capture
|
||
The agenda is nice, but a souped up version is nicer.
|
||
#+begin_src emacs-lisp
|
||
(package! org-super-agenda :pin "f5e80e4d0da6b2eeda9ba21e021838fa6a495376")
|
||
#+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")
|
||
:pin "8ac08633ae413a6605b6506d2739eece7475272e")
|
||
#+end_src
|
||
**** Visuals
|
||
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...
|
||
#+begin_src emacs-lisp
|
||
(package! org-pretty-table
|
||
:recipe (:host github :repo "Fuco1/org-pretty-table") :pin "474ad84a8fe5377d67ab7e491e8e68dac6e37a11")
|
||
#+end_src
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! org-pretty-table
|
||
:commands (org-pretty-table-mode global-org-pretty-table-mode))
|
||
#+end_src
|
||
|
||
For automatically toggling LaTeX fragment previews there's this nice package
|
||
#+begin_src emacs-lisp
|
||
(package! org-fragtog :pin "0151cabc7aa9f244f82e682b87713b344d780c23")
|
||
#+end_src
|
||
|
||
Then for pretty markers
|
||
#+begin_src emacs-lisp
|
||
(package! org-appear :recipe (:host github :repo "awth13/org-appear")
|
||
:pin "19ea96e6e2ce01b8583b25a6e5579f1be207a119")
|
||
#+end_src
|
||
~org-superstar-mode~ is great. While we're at it we may as well make tags prettier as well :)
|
||
#+begin_src emacs-lisp
|
||
(package! org-pretty-tags :pin "5c7521651b35ae9a7d3add4a66ae8cc176ae1c76")
|
||
#+end_src
|
||
|
||
There's this nice package that can provide nice syntax highlighting with LaTeX
|
||
exports.
|
||
#+begin_src emacs-lisp
|
||
(package! engrave-faces :recipe (:local-repo "lisp/engrave-faces"))
|
||
#+end_src
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! engrave-faces-latex
|
||
:after ox-latex)
|
||
#+end_src
|
||
**** Extra functionality
|
||
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.
|
||
#+begin_src emacs-lisp
|
||
(package! ox-gfm :pin "99f93011b069e02b37c9660b8fcb45dab086a07f")
|
||
#+end_src
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! ox-gfm
|
||
:after org)
|
||
#+end_src
|
||
|
||
Now and then citations need to happen
|
||
#+begin_src emacs-lisp
|
||
(package! org-ref :pin "113506df694d65e065534e516db1c592d61f44b7")
|
||
#+end_src
|
||
|
||
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")
|
||
#+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
|
||
(package! org-chef :pin "5b461ed7d458cdcbff0af5013fbdbe88cbfb13a4")
|
||
#+end_src
|
||
|
||
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.
|
||
#+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)
|
||
#+end_src
|
||
|
||
Org-roam is nice by itself, but there are so /extra/ nice packages which integrate
|
||
with it.
|
||
#+begin_src emacs-lisp
|
||
(package! org-roam-server :pin "2093ea5a1a1f2d128dd377778472a481913717b4")
|
||
#+end_src
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package org-roam-server
|
||
:after (org-roam server)
|
||
: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-network-label-truncate t
|
||
org-roam-server-network-label-truncate-length 60
|
||
org-roam-server-network-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
|
||
*** Systemd
|
||
For editing systemd unit files
|
||
#+begin_src emacs-lisp
|
||
(package! systemd :pin "b6ae63a236605b1c5e1069f7d3afe06ae32a7bae")
|
||
#+end_src
|
||
*** Graphviz
|
||
Graphviz is a nice method of visualising simple graphs, based on plaintext
|
||
=.dot= / =.gv= files.
|
||
#+begin_src emacs-lisp
|
||
(package! graphviz-dot-mode :pin "3642a0a5f41a80c8ecef7c6143d514200b80e194")
|
||
#+end_src
|
||
*** Authinfo
|
||
#+begin_src emacs-lisp
|
||
(package! authinfo-color-mode
|
||
:recipe (:local-repo "lisp/authinfo-color-mode"))
|
||
#+end_src
|
||
Now we just need to load it appropriately.
|
||
#+begin_src emacs-lisp :tangle yes
|
||
(use-package! authinfo-color-mode
|
||
:mode ("authinfo.gpg\\'" . authinfo-color-mode)
|
||
:init (advice-add 'authinfo-mode :override #'authinfo-color-mode))
|
||
#+end_src
|
||
*** Beancount (accounting)
|
||
#+begin_src emacs-lisp
|
||
(package! beancount :recipe (:host github :repo "beancount/beancount-mode")
|
||
:pin "7a0ef01d1ff6f8c318af944131310ca06d4c65ff")
|
||
#+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
|
||
** Avy
|
||
What a wonderful way to jump to buffer positions, and it uses the QWERTY
|
||
home-row for jumping. Very convenient ... except I'm using Colemak.
|
||
|
||
#+begin_src emacs-lisp :tangle (if (= 0 (call-process "sh" nil nil nil "-c" "dmesg | grep -q 'ErgoDox'")) "yes" "no")
|
||
(after! avy
|
||
;; home row priorities: 8 6 4 5 - - 1 2 3 7
|
||
(setq avy-keys '(?n ?e ?i ?s ?t ?r ?i ?a)))
|
||
#+end_src
|
||
** Calc
|
||
*** Defaults
|
||
Any sane person prefers radians and exact values.
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq calc-angle-mode 'rad ; radians are rad
|
||
calc-symbolic-mode t) ; keeps expressions like \sqrt{2} irrational for as long as possible
|
||
#+end_src
|
||
|
||
#+attr_html: :class invertible :alt Demonstration of calc, prettified by calctex.
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/calc-with-calctex.png]]
|
||
|
||
*** CalcTeX
|
||
|
||
We'd like to use CalcTeX too, so let's set that up, and fix some glaring
|
||
inadequacies --- why on earth would you commit a hard-coded path to an executable
|
||
that /only works on your local machine/, consequently breaking the package for
|
||
everyone else!?
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package! calctex
|
||
:commands calctex-mode
|
||
:init
|
||
(add-hook 'calc-mode-hook #'calctex-mode)
|
||
:config
|
||
(setq calctex-additional-latex-packages "
|
||
\\usepackage[usenames]{color}
|
||
\\usepackage{xcolor}
|
||
\\usepackage{soul}
|
||
\\usepackage{adjustbox}
|
||
\\usepackage{amsmath}
|
||
\\usepackage{amssymb}
|
||
\\usepackage{siunitx}
|
||
\\usepackage{cancel}
|
||
\\usepackage{mathtools}
|
||
\\usepackage{mathalpha}
|
||
\\usepackage{xparse}
|
||
\\usepackage{arevmath}"
|
||
calctex-additional-latex-macros
|
||
(concat calctex-additional-latex-macros
|
||
"\n\\let\\evalto\\Rightarrow"))
|
||
(defadvice! no-messaging-a (orig-fn &rest args)
|
||
:around #'calctex-default-dispatching-render-process
|
||
(let ((inhibit-message t) message-log-max)
|
||
(apply orig-fn args)))
|
||
;; Fix hardcoded dvichop path (whyyyyyyy)
|
||
(let ((vendor-folder (concat (file-truename doom-local-dir)
|
||
"straight/"
|
||
(format "build-%s" emacs-version)
|
||
"/calctex/vendor/")))
|
||
(setq calctex-dvichop-sty (concat vendor-folder "texd/dvichop")
|
||
calctex-dvichop-bin (concat vendor-folder "texd/dvichop")))
|
||
(unless (file-exists-p calctex-dvichop-bin)
|
||
(message "CalcTeX: Building dvichop binary")
|
||
(let ((default-directory (file-name-directory calctex-dvichop-bin)))
|
||
(call-process "make" nil nil nil))))
|
||
#+end_src
|
||
|
||
*** Embedded calc
|
||
|
||
Embedded calc is a lovely feature which let's us use calc to operate on LaTeX
|
||
maths expressions. The standard keybinding is a bit janky however (=C-x * e=), so
|
||
we'll add a localleader-based alternative.
|
||
|
||
#+begin_src emacs-lisp
|
||
(map! :map calc-mode-map
|
||
:after calc
|
||
:localleader
|
||
:desc "Embedded calc (toggle)" "e" #'calc-embedded)
|
||
(map! :map org-mode-map
|
||
:after org
|
||
:localleader
|
||
:desc "Embedded calc (toggle)" "E" #'calc-embedded)
|
||
(map! :map latex-mode-map
|
||
:after latex
|
||
:localleader
|
||
:desc "Embedded calc (toggle)" "e" #'calc-embedded)
|
||
#+end_src
|
||
|
||
Unfortunately this operates without the (rather informative) calculator and
|
||
trail buffers, but we can advice it that we would rather like those in a side
|
||
panel.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar calc-embedded-trail-window nil)
|
||
(defvar calc-embedded-calculator-window nil)
|
||
|
||
(defadvice! calc-embedded-with-side-pannel (&rest _)
|
||
:after #'calc-do-embedded
|
||
(when calc-embedded-trail-window
|
||
(ignore-errors
|
||
(delete-window calc-embedded-trail-window))
|
||
(setq calc-embedded-trail-window nil))
|
||
(when calc-embedded-calculator-window
|
||
(ignore-errors
|
||
(delete-window calc-embedded-calculator-window))
|
||
(setq calc-embedded-calculator-window nil))
|
||
(when (and calc-embedded-info
|
||
(> (* (window-width) (window-height)) 1200))
|
||
(let ((main-window (selected-window))
|
||
(vertical-p (> (window-width) 80)))
|
||
(select-window
|
||
(setq calc-embedded-trail-window
|
||
(if vertical-p
|
||
(split-window-horizontally (- (max 30 (/ (window-width) 3))))
|
||
(split-window-vertically (- (max 8 (/ (window-height) 4)))))))
|
||
(switch-to-buffer "*Calc Trail*")
|
||
(select-window
|
||
(setq calc-embedded-calculator-window
|
||
(if vertical-p
|
||
(split-window-vertically -6)
|
||
(split-window-horizontally (- (/ (window-width) 2))))))
|
||
(switch-to-buffer "*Calculator*")
|
||
(select-window main-window))))
|
||
#+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
|
||
(centaur-tabs-mode -1)
|
||
(setq centaur-tabs-height 36
|
||
centaur-tabs-set-icons t
|
||
centaur-tabs-modified-marker "o"
|
||
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)
|
||
(setq company-show-numbers t)
|
||
(add-hook 'evil-normal-state-entry-hook #'company-abort)) ;; make aborting less annoying.
|
||
#+end_src
|
||
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
|
||
~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
|
||
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
|
||
** Elcord
|
||
#+begin_src emacs-lisp
|
||
(setq elcord-use-major-mode-as-main-icon t)
|
||
#+end_src
|
||
** Emacs Everywhere
|
||
#+begin_src emacs-lisp
|
||
(when (daemonp)
|
||
(require 'spell-fu)
|
||
(setq emacs-everywhere-major-mode-function #'org-mode
|
||
emacs-everywhere-frame-name-format "Edit ∷ %s — %s")
|
||
(require 'emacs-everywhere))
|
||
#+end_src
|
||
** Emojify
|
||
|
||
For starters, twitter's emojis look nicer than emoji-one.
|
||
Other than that, this is pretty great OOTB.
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq emojify-emoji-set "twemoji-v2")
|
||
#+end_src
|
||
|
||
One minor annoyance is the use of emojis over the default character
|
||
when the default is actually preferred. This occurs with overlay symbols I use
|
||
in Org mode, such as checkbox state, and a few other miscellaneous cases.
|
||
|
||
We can accommodate our preferences by deleting those entries from the emoji hash
|
||
table
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar emojify-disabled-emojis
|
||
'(;; Org
|
||
"◼" "☑" "☸" "⚙" "⏩" "⏪" "⬆" "⬇" "❓"
|
||
;; Terminal powerline
|
||
"✔"
|
||
;; Box drawing
|
||
"▶" "◀")
|
||
"Charachters that should never be affected by `emojify-mode'.")
|
||
|
||
(defadvice! emojify-delete-from-data ()
|
||
"Ensure `emojify-disabled-emojis' don't appear in `emojify-emojis'."
|
||
:after #'emojify-set-emoji-data
|
||
(dolist (emoji emojify-disabled-emojis)
|
||
(remhash emoji emojify-emojis)))
|
||
#+end_src
|
||
|
||
#+end_src
|
||
|
||
This new minor mode of ours will be nice for messages, so let's hook it in for
|
||
Email and IRC.
|
||
#+begin_src emacs-lisp
|
||
(add-hook! '(mu4e-compose-mode org-msg-edit-mode circe-channel-mode) (emoticon-to-emoji 1))
|
||
#+end_src
|
||
|
||
** 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-escape (evil-escape-mode -1))
|
||
#+end_src
|
||
|
||
When I want to make a substitution, I want it to be global more often than not
|
||
--- so let's make that the default.
|
||
#+begin_src emacs-lisp
|
||
(after! evil (setq evil-ex-substitute-global t)) ; I like my s/../.. to by global by default
|
||
#+end_src
|
||
** Info colours
|
||
#+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
|
||
|
||
#+attr_html: :class invertible :alt Example colourised info page
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/info-coloured.png]]
|
||
** Ispell
|
||
*** Downloading dictionaries
|
||
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
|
||
|
||
**** Hunspell
|
||
#+begin_src shell :tangle (if (file-exists-p "/usr/share/myspell/en-custom.dic") "no" "setup.sh")
|
||
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")
|
||
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
|
||
#+begin_src emacs-lisp
|
||
(setq ispell-dictionary "en-custom")
|
||
#+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
|
||
(setq ispell-personal-dictionary (expand-file-name ".ispell_personal" doom-private-dir))
|
||
#+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
|
||
** 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.
|
||
** Mixed pitch
|
||
#+begin_src emacs-lisp
|
||
(autoload #'mixed-pitch-serif-mode "mixed-pitch"
|
||
"Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch." t)
|
||
|
||
(after! mixed-pitch
|
||
(defface variable-pitch-serif
|
||
'((t (:family "serif")))
|
||
"A variable-pitch face with serifs."
|
||
:group 'basic-faces)
|
||
(setq mixed-pitch-set-height t)
|
||
(setq variable-pitch-serif-font (font-spec :family "Alegreya" :size 27))
|
||
(set-face-attribute 'variable-pitch-serif nil :font variable-pitch-serif-font)
|
||
(defun mixed-pitch-serif-mode (&optional arg)
|
||
"Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch."
|
||
(interactive)
|
||
(let ((mixed-pitch-face 'variable-pitch-serif))
|
||
(mixed-pitch-mode (or arg 'toggle)))))
|
||
#+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
|
||
** 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
|
||
** 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
|
||
Let's automatically update terminals on theme change (as long as ~pywal~ is available).
|
||
|
||
Unfortunately, as the theme is set on startup this causes the hook to be run
|
||
immediately. It would be nicer to /not/ have this add to our precious startup
|
||
time (around 0.4s last time I checked).
|
||
We can achieve this by deferring it with a short idle timer that should add the
|
||
hook /just after/ initialisation.
|
||
|
||
#+begin_src emacs-lisp :tangle (if (executable-find "wal") "yes" "no")
|
||
(run-with-idle-timer 0.1 nil (lambda () (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")
|
||
(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
|
||
*** Guix
|
||
[[https://guix.gnu.org/][Guix]] puts some binaries that TRAMP looks for in unexpected locations.
|
||
That's no problem though, we just need to help TRAMP find them.
|
||
#+begin_src emacs-lisp
|
||
(after! tramp
|
||
(appendq! tramp-remote-path
|
||
'("~/.guix-profile/bin" "~/.guix-profile/sbin"
|
||
"/run/current-system/profile/bin"
|
||
"/run/current-system/profile/sbin")))
|
||
#+end_src
|
||
** Treemacs
|
||
Quite often there are superfluous files I'm not that interested in. There's no
|
||
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.
|
||
#+begin_src emacs-lisp
|
||
(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"
|
||
"*/_region_.tex"))
|
||
#+end_src
|
||
** Which-key
|
||
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
|
||
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.
|
||
#+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
|
||
|
||
#+attr_html: :class invertible :alt Whichkey triggered on an evil motion
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/whichkey-evil.png]]
|
||
** Writeroom
|
||
For starters, I think Doom is a bit over-zealous when zooming in
|
||
#+begin_src emacs-lisp
|
||
(setq +zen-text-scale 0.8)
|
||
#+end_src
|
||
|
||
Then, when using Org it would be nice to make a number of other aesthetic
|
||
tweaks. Namely:
|
||
+ Use a serifed variable-pitch font
|
||
+ Hiding headline leading stars
|
||
+ Using fleurons as headline bullets
|
||
+ Hiding line numbers
|
||
+ Centring the text
|
||
+ Turning on ~org-pretty-table-mode~
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar +zen-serif-p t
|
||
"Whether to use a serifed font with `mixed-pitch-mode'.")
|
||
(after! writeroom-mode
|
||
(defvar-local +zen--original-org-indent-mode-p nil)
|
||
(defvar-local +zen--original-mixed-pitch-mode-p nil)
|
||
(defvar-local +zen--original-solaire-mode-p nil)
|
||
(defvar-local +zen--original-org-pretty-table-mode-p nil)
|
||
(defun +zen-enable-mixed-pitch-mode-h ()
|
||
"Enable `mixed-pitch-mode' when in `+zen-mixed-pitch-modes'."
|
||
(when (apply #'derived-mode-p +zen-mixed-pitch-modes)
|
||
(if writeroom-mode
|
||
(progn
|
||
(setq +zen--original-solaire-mode-p solaire-mode)
|
||
(solaire-mode -1)
|
||
(setq +zen--original-mixed-pitch-mode-p mixed-pitch-mode)
|
||
(funcall (if +zen-serif-p #'mixed-pitch-serif-mode #'mixed-pitch-mode) 1))
|
||
(funcall #'mixed-pitch-mode (if +zen--original-mixed-pitch-mode-p 1 -1))
|
||
(when +zen--original-solaire-mode-p (solaire-mode 1)))))
|
||
(pushnew! writeroom--local-variables
|
||
'display-line-numbers
|
||
'visual-fill-column-width)
|
||
(add-hook 'writeroom-mode-enable-hook
|
||
(defun +zen-prose-org-h ()
|
||
"Reformat the current Org buffer appearance for prose."
|
||
(when (and (eq major-mode 'org-mode) writeroom-mode)
|
||
(setq display-line-numbers nil
|
||
visual-fill-column-width 60)
|
||
(when (featurep 'org-superstar)
|
||
(setq-local org-superstar-headline-bullets-list '("🙘" "🙙" "🙚" "🙛")
|
||
;; org-superstar-headline-bullets-list '("🙐" "🙑" "🙒" "🙓" "🙔" "🙕" "🙖" "🙗")
|
||
org-superstar-remove-leading-stars t)
|
||
(org-superstar-restart))
|
||
(setq
|
||
+zen--original-org-indent-mode-p org-indent-mode
|
||
+zen--original-org-pretty-table-mode-p (bound-and-true-p org-pretty-table-mode))
|
||
(org-indent-mode -1)
|
||
(org-pretty-table-mode 1))))
|
||
(add-hook 'writeroom-mode-disable-hook
|
||
(defun +zen-nonprose-org-h ()
|
||
"Reverse the effect of `+zen-prose-org'."
|
||
(when (eq major-mode 'org-mode)
|
||
(when (featurep 'org-superstar)
|
||
(org-superstar-restart))
|
||
(when +zen--original-org-indent-mode-p (org-indent-mode 1))
|
||
;; (unless +zen--original-org-pretty-table-mode-p (org-pretty-table-mode -1))
|
||
))))
|
||
#+end_src
|
||
|
||
#+attr_html: :class invertible :alt Writeroom applied to an Org file
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/writeroom-and-org.png]]
|
||
** xkcd
|
||
|
||
We want to set this up so it loads nicely in [[*Extra links][Extra links]].
|
||
#+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
|
||
(setq xkcd-cache-dir (expand-file-name "xkcd/" doom-cache-dir)
|
||
xkcd-cache-latest (concat xkcd-cache-dir "latest"))
|
||
(unless (file-exists-p xkcd-cache-dir)
|
||
(make-directory xkcd-cache-dir))
|
||
(after! evil-snipe
|
||
(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))
|
||
)))))
|
||
#+end_src
|
||
** YASnippet
|
||
Nested snippets are good, enable that.
|
||
#+begin_src emacs-lisp
|
||
(setq yas-triggers-in-field t)
|
||
#+end_src
|
||
* Applications
|
||
** Ebooks
|
||
[[xkcd:548]]
|
||
=calibredb= lets us use calibre through Emacs, because who wouldn't want to use
|
||
something through Emacs?
|
||
#+begin_src emacs-lisp
|
||
(use-package! calibredb
|
||
:commands calibredb
|
||
:config
|
||
(setq calibredb-root-dir "~/Desktop/TEC/Other/Ebooks"
|
||
calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
|
||
(map! :map calibredb-show-mode-map
|
||
:ne "?" #'calibredb-entry-dispatch
|
||
:ne "o" #'calibredb-find-file
|
||
:ne "O" #'calibredb-find-file-other-frame
|
||
:ne "V" #'calibredb-open-file-with-default-tool
|
||
:ne "s" #'calibredb-set-metadata-dispatch
|
||
:ne "e" #'calibredb-export-dispatch
|
||
:ne "q" #'calibredb-entry-quit
|
||
:ne "." #'calibredb-open-dired
|
||
:ne [tab] #'calibredb-toggle-view-at-point
|
||
:ne "M-t" #'calibredb-set-metadata--tags
|
||
:ne "M-a" #'calibredb-set-metadata--author_sort
|
||
:ne "M-A" #'calibredb-set-metadata--authors
|
||
:ne "M-T" #'calibredb-set-metadata--title
|
||
:ne "M-c" #'calibredb-set-metadata--comments)
|
||
(map! :map calibredb-search-mode-map
|
||
:ne [mouse-3] #'calibredb-search-mouse
|
||
:ne "RET" #'calibredb-find-file
|
||
:ne "?" #'calibredb-dispatch
|
||
:ne "a" #'calibredb-add
|
||
:ne "A" #'calibredb-add-dir
|
||
:ne "c" #'calibredb-clone
|
||
:ne "d" #'calibredb-remove
|
||
:ne "D" #'calibredb-remove-marked-items
|
||
:ne "j" #'calibredb-next-entry
|
||
:ne "k" #'calibredb-previous-entry
|
||
:ne "l" #'calibredb-virtual-library-list
|
||
:ne "L" #'calibredb-library-list
|
||
:ne "n" #'calibredb-virtual-library-next
|
||
:ne "N" #'calibredb-library-next
|
||
:ne "p" #'calibredb-virtual-library-previous
|
||
:ne "P" #'calibredb-library-previous
|
||
:ne "s" #'calibredb-set-metadata-dispatch
|
||
:ne "S" #'calibredb-switch-library
|
||
:ne "o" #'calibredb-find-file
|
||
:ne "O" #'calibredb-find-file-other-frame
|
||
:ne "v" #'calibredb-view
|
||
:ne "V" #'calibredb-open-file-with-default-tool
|
||
:ne "." #'calibredb-open-dired
|
||
:ne "b" #'calibredb-catalog-bib-dispatch
|
||
:ne "e" #'calibredb-export-dispatch
|
||
:ne "r" #'calibredb-search-refresh-and-clear-filter
|
||
:ne "R" #'calibredb-search-clear-filter
|
||
:ne "q" #'calibredb-search-quit
|
||
:ne "m" #'calibredb-mark-and-forward
|
||
:ne "f" #'calibredb-toggle-favorite-at-point
|
||
:ne "x" #'calibredb-toggle-archive-at-point
|
||
:ne "h" #'calibredb-toggle-highlight-at-point
|
||
:ne "u" #'calibredb-unmark-and-forward
|
||
:ne "i" #'calibredb-edit-annotation
|
||
:ne "DEL" #'calibredb-unmark-and-backward
|
||
:ne [backtab] #'calibredb-toggle-view
|
||
:ne [tab] #'calibredb-toggle-view-at-point
|
||
:ne "M-n" #'calibredb-show-next-entry
|
||
:ne "M-p" #'calibredb-show-previous-entry
|
||
:ne "/" #'calibredb-search-live-filter
|
||
:ne "M-t" #'calibredb-set-metadata--tags
|
||
:ne "M-a" #'calibredb-set-metadata--author_sort
|
||
:ne "M-A" #'calibredb-set-metadata--authors
|
||
:ne "M-T" #'calibredb-set-metadata--title
|
||
:ne "M-c" #'calibredb-set-metadata--comments))
|
||
#+end_src
|
||
|
||
Then, to actually read the ebooks we use =nov=.
|
||
|
||
#+attr_html: :class invertible :alt Excerpt of the GNU Emacs manual viewed through nov.el
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/nov.png]]
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package! nov
|
||
:mode ("\\.epub\\'" . nov-mode)
|
||
:config
|
||
(map! :map nov-mode-map
|
||
:n "RET" #'nov-scroll-up)
|
||
|
||
(defun doom-modeline-segment--nov-info ()
|
||
(concat
|
||
" "
|
||
(propertize
|
||
(cdr (assoc 'creator nov-metadata))
|
||
'face 'doom-modeline-project-parent-dir)
|
||
" "
|
||
(cdr (assoc 'title nov-metadata))
|
||
" "
|
||
(propertize
|
||
(format "%d/%d"
|
||
(1+ nov-documents-index)
|
||
(length nov-documents))
|
||
'face 'doom-modeline-info)))
|
||
|
||
(advice-add 'nov-render-title :override #'ignore)
|
||
|
||
(defun +nov-mode-setup ()
|
||
(face-remap-add-relative 'variable-pitch
|
||
:family "Merriweather"
|
||
:height 1.4
|
||
:width 'semi-expanded)
|
||
(face-remap-add-relative 'default :height 1.3)
|
||
(setq-local line-spacing 0.2
|
||
next-screen-context-lines 4
|
||
shr-use-colors nil)
|
||
(require 'visual-fill-column nil t)
|
||
(setq-local visual-fill-column-center-text t
|
||
visual-fill-column-width 80
|
||
nov-text-width 80)
|
||
(visual-fill-column-mode 1)
|
||
(hl-line-mode -1)
|
||
|
||
(add-to-list '+lookup-definition-functions #'+lookup/dictionary-definition)
|
||
|
||
(setq-local mode-line-format
|
||
`((:eval
|
||
(doom-modeline-segment--workspace-name))
|
||
(:eval
|
||
(doom-modeline-segment--window-number))
|
||
(:eval
|
||
(doom-modeline-segment--nov-info))
|
||
,(propertize
|
||
" %P "
|
||
'face 'doom-modeline-buffer-minor-mode)
|
||
,(propertize
|
||
" "
|
||
'face (if (doom-modeline--active) 'mode-line 'mode-line-inactive)
|
||
'display `((space
|
||
:align-to
|
||
(- (+ right right-fringe right-margin)
|
||
,(* (let ((width (doom-modeline--font-width)))
|
||
(or (and (= width 1) 1)
|
||
(/ width (frame-char-width) 1.0)))
|
||
(string-width
|
||
(format-mode-line (cons "" '(:eval (doom-modeline-segment--major-mode))))))))))
|
||
(:eval (doom-modeline-segment--major-mode)))))
|
||
|
||
(add-hook 'nov-mode-hook #'+nov-mode-setup))
|
||
#+end_src
|
||
** IRC
|
||
=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.
|
||
|
||
[[xkcd:1782]]
|
||
|
||
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
|
||
(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
|
||
(s-split "," (plist-get entry :port))))
|
||
(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))))
|
||
#+end_src
|
||
|
||
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 no-export
|
||
(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))))
|
||
|
||
<<irc-authinfo-reader>>
|
||
|
||
(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
|
||
** Newsfeed
|
||
RSS feeds are still a thing. Why not make use of them with =elfeed=.
|
||
I really like what [[https://github.com/fuxialexander/doom-emacs-private-xfu/tree/master/modules/app/rss][fuxialexander]] has going on, but I don't think I need a custom
|
||
module. Let's just try to patch on the main things I like the look of.
|
||
|
||
#+attr_html: :class invertible :alt Example elfeed entry
|
||
[[https://tecosaur.com/lfs/emacs-config/screenshots/elfeed.png]]
|
||
|
||
*** Keybindings
|
||
#+begin_src emacs-lisp
|
||
(map! :map elfeed-search-mode-map
|
||
:after elfeed-search
|
||
[remap kill-this-buffer] "q"
|
||
[remap kill-buffer] "q"
|
||
:n doom-leader-key nil
|
||
:n "q" #'+rss/quit
|
||
:n "e" #'elfeed-update
|
||
:n "r" #'elfeed-search-untag-all-unread
|
||
:n "u" #'elfeed-search-tag-all-unread
|
||
:n "s" #'elfeed-search-live-filter
|
||
:n "RET" #'elfeed-search-show-entry
|
||
:n "p" #'elfeed-show-pdf
|
||
:n "+" #'elfeed-search-tag-all
|
||
:n "-" #'elfeed-search-untag-all
|
||
:n "S" #'elfeed-search-set-filter
|
||
:n "b" #'elfeed-search-browse-url
|
||
:n "y" #'elfeed-search-yank)
|
||
(map! :map elfeed-show-mode-map
|
||
:after elfeed-show
|
||
[remap kill-this-buffer] "q"
|
||
[remap kill-buffer] "q"
|
||
:n doom-leader-key nil
|
||
:nm "q" #'+rss/delete-pane
|
||
:nm "o" #'ace-link-elfeed
|
||
:nm "RET" #'org-ref-elfeed-add
|
||
:nm "n" #'elfeed-show-next
|
||
:nm "N" #'elfeed-show-prev
|
||
:nm "p" #'elfeed-show-pdf
|
||
:nm "+" #'elfeed-show-tag
|
||
:nm "-" #'elfeed-show-untag
|
||
:nm "s" #'elfeed-show-new-live-search
|
||
:nm "y" #'elfeed-show-yank)
|
||
#+end_src
|
||
*** Usability enhancements
|
||
#+begin_src emacs-lisp
|
||
(after! elfeed-search
|
||
(set-evil-initial-state! 'elfeed-search-mode 'normal))
|
||
(after! elfeed-show-mode
|
||
(set-evil-initial-state! 'elfeed-show-mode 'normal))
|
||
|
||
(after! evil-snipe
|
||
(push 'elfeed-show-mode evil-snipe-disabled-modes)
|
||
(push 'elfeed-search-mode evil-snipe-disabled-modes))
|
||
#+end_src
|
||
*** Visual enhancements
|
||
#+begin_src emacs-lisp
|
||
(after! elfeed
|
||
|
||
(elfeed-org)
|
||
(use-package! elfeed-link)
|
||
|
||
(setq elfeed-search-filter "@1-week-ago +unread"
|
||
elfeed-search-print-entry-function '+rss/elfeed-search-print-entry
|
||
elfeed-search-title-min-width 80
|
||
elfeed-show-entry-switch #'pop-to-buffer
|
||
elfeed-show-entry-delete #'+rss/delete-pane
|
||
elfeed-show-refresh-function #'+rss/elfeed-show-refresh--better-style
|
||
shr-max-image-proportion 0.6)
|
||
|
||
(add-hook! 'elfeed-show-mode-hook (hide-mode-line-mode 1))
|
||
(add-hook! 'elfeed-search-update-hook #'hide-mode-line-mode)
|
||
|
||
(defface elfeed-show-title-face '((t (:weight ultrabold :slant italic :height 1.5)))
|
||
"title face in elfeed show buffer"
|
||
:group 'elfeed)
|
||
(defface elfeed-show-author-face `((t (:weight light)))
|
||
"title face in elfeed show buffer"
|
||
:group 'elfeed)
|
||
(set-face-attribute 'elfeed-search-title-face nil
|
||
:foreground 'nil
|
||
:weight 'light)
|
||
|
||
(defadvice! +rss-elfeed-wrap-h-nicer ()
|
||
"Enhances an elfeed entry's readability by wrapping it to a width of
|
||
`fill-column' and centering it with `visual-fill-column-mode'."
|
||
:override #'+rss-elfeed-wrap-h
|
||
(let ((inhibit-read-only t)
|
||
(inhibit-modification-hooks t))
|
||
(setq-local truncate-lines nil)
|
||
(setq-local shr-width 120)
|
||
(setq-local line-spacing 0.2)
|
||
(setq-local visual-fill-column-center-text t)
|
||
(visual-fill-column-mode)
|
||
;; (setq-local shr-current-font '(:family "Merriweather" :height 1.2))
|
||
(set-buffer-modified-p nil)))
|
||
|
||
(defun +rss/elfeed-search-print-entry (entry)
|
||
"Print ENTRY to the buffer."
|
||
(let* ((elfeed-goodies/tag-column-width 40)
|
||
(elfeed-goodies/feed-source-column-width 30)
|
||
(title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
|
||
(title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
|
||
(feed (elfeed-entry-feed entry))
|
||
(feed-title
|
||
(when feed
|
||
(or (elfeed-meta feed :title) (elfeed-feed-title feed))))
|
||
(tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
|
||
(tags-str (concat (mapconcat 'identity tags ",")))
|
||
(title-width (- (window-width) elfeed-goodies/feed-source-column-width
|
||
elfeed-goodies/tag-column-width 4))
|
||
|
||
(tag-column (elfeed-format-column
|
||
tags-str (elfeed-clamp (length tags-str)
|
||
elfeed-goodies/tag-column-width
|
||
elfeed-goodies/tag-column-width)
|
||
:left))
|
||
(feed-column (elfeed-format-column
|
||
feed-title (elfeed-clamp elfeed-goodies/feed-source-column-width
|
||
elfeed-goodies/feed-source-column-width
|
||
elfeed-goodies/feed-source-column-width)
|
||
:left)))
|
||
|
||
(insert (propertize feed-column 'face 'elfeed-search-feed-face) " ")
|
||
(insert (propertize tag-column 'face 'elfeed-search-tag-face) " ")
|
||
(insert (propertize title 'face title-faces 'kbd-help title))
|
||
(setq-local line-spacing 0.2)))
|
||
|
||
(defun +rss/elfeed-show-refresh--better-style ()
|
||
"Update the buffer to match the selected entry, using a mail-style."
|
||
(interactive)
|
||
(let* ((inhibit-read-only t)
|
||
(title (elfeed-entry-title elfeed-show-entry))
|
||
(date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
|
||
(author (elfeed-meta elfeed-show-entry :author))
|
||
(link (elfeed-entry-link elfeed-show-entry))
|
||
(tags (elfeed-entry-tags elfeed-show-entry))
|
||
(tagsstr (mapconcat #'symbol-name tags ", "))
|
||
(nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
|
||
(content (elfeed-deref (elfeed-entry-content elfeed-show-entry)))
|
||
(type (elfeed-entry-content-type elfeed-show-entry))
|
||
(feed (elfeed-entry-feed elfeed-show-entry))
|
||
(feed-title (elfeed-feed-title feed))
|
||
(base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
|
||
(erase-buffer)
|
||
(insert "\n")
|
||
(insert (format "%s\n\n" (propertize title 'face 'elfeed-show-title-face)))
|
||
(insert (format "%s\t" (propertize feed-title 'face 'elfeed-search-feed-face)))
|
||
(when (and author elfeed-show-entry-author)
|
||
(insert (format "%s\n" (propertize author 'face 'elfeed-show-author-face))))
|
||
(insert (format "%s\n\n" (propertize nicedate 'face 'elfeed-log-date-face)))
|
||
(when tags
|
||
(insert (format "%s\n"
|
||
(propertize tagsstr 'face 'elfeed-search-tag-face))))
|
||
;; (insert (propertize "Link: " 'face 'message-header-name))
|
||
;; (elfeed-insert-link link link)
|
||
;; (insert "\n")
|
||
(cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
|
||
do (insert (propertize "Enclosure: " 'face 'message-header-name))
|
||
do (elfeed-insert-link (car enclosure))
|
||
do (insert "\n"))
|
||
(insert "\n")
|
||
(if content
|
||
(if (eq type 'html)
|
||
(elfeed-insert-html content base)
|
||
(insert content))
|
||
(insert (propertize "(empty)\n" 'face 'italic)))
|
||
(goto-char (point-min))))
|
||
|
||
)
|
||
#+end_src
|
||
*** Functionality enhancements
|
||
#+begin_src emacs-lisp
|
||
(after! elfeed-show
|
||
(require 'url)
|
||
|
||
(defvar elfeed-pdf-dir
|
||
(expand-file-name "pdfs/"
|
||
(file-name-directory (directory-file-name elfeed-enclosure-default-dir))))
|
||
|
||
(defvar elfeed-link-pdfs
|
||
'(("https://www.jstatsoft.org/index.php/jss/article/view/v0\\([^/]+\\)" . "https://www.jstatsoft.org/index.php/jss/article/view/v0\\1/v\\1.pdf")
|
||
("http://arxiv.org/abs/\\([^/]+\\)" . "https://arxiv.org/pdf/\\1.pdf"))
|
||
"List of alists of the form (REGEX-FOR-LINK . FORM-FOR-PDF)")
|
||
|
||
(defun elfeed-show-pdf (entry)
|
||
(interactive
|
||
(list (or elfeed-show-entry (elfeed-search-selected :ignore-region))))
|
||
(let ((link (elfeed-entry-link entry))
|
||
(feed-name (plist-get (elfeed-feed-meta (elfeed-entry-feed entry)) :title))
|
||
(title (elfeed-entry-title entry))
|
||
(file-view-function
|
||
(lambda (f)
|
||
(when elfeed-show-entry
|
||
(elfeed-kill-buffer))
|
||
(pop-to-buffer (find-file-noselect f))))
|
||
pdf)
|
||
|
||
(let ((file (expand-file-name
|
||
(concat (subst-char-in-string ?/ ?, title) ".pdf")
|
||
(expand-file-name (subst-char-in-string ?/ ?, feed-name)
|
||
elfeed-pdf-dir))))
|
||
(if (file-exists-p file)
|
||
(funcall file-view-function file)
|
||
(dolist (link-pdf elfeed-link-pdfs)
|
||
(when (and (string-match-p (car link-pdf) link)
|
||
(not pdf))
|
||
(setq pdf (replace-regexp-in-string (car link-pdf) (cdr link-pdf) link))))
|
||
(if (not pdf)
|
||
(message "No associated PDF for entry")
|
||
(message "Fetching %s" pdf)
|
||
(unless (file-exists-p (file-name-directory file))
|
||
(make-directory (file-name-directory file) t))
|
||
(url-copy-file pdf file)
|
||
(funcall file-view-function file))))))
|
||
|
||
)
|
||
#+end_src
|
||
** Dictionary
|
||
We start off by 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 "E" (cmd! (lexic-return-from-lexic) ; expand
|
||
(switch-to-buffer (lexic-get-buffer)))
|
||
:n "M" (cmd! (lexic-return-from-lexic) ; minimise
|
||
(lexic-goto-lexic))
|
||
:n "C-p" #'lexic-search-history-backwards
|
||
:n "C-n" #'lexic-search-history-forwards
|
||
:n "/" (cmd! (call-interactively #'lexic-search))))
|
||
#+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
|
||
** 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")
|
||
go get -u gitlab.com/shackra/goimapnotify
|
||
ln -s ~/.local/share/go/bin/goimapnotify ~/.local/bin/
|
||
#+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
|
||
"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 "~/.config/imapnotify") "no" "setup.sh")
|
||
~/.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("~/.config/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"
|
||
)
|
||
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,
|
||
)
|
||
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")
|
||
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] in ["-e", "--enable"]:
|
||
enableAccountSystemdServices(oldAccounts)
|
||
exit()
|
||
elif sys.argv[1] in ["-d", "--disable"]:
|
||
disableAccountSystemdServices(oldAccounts)
|
||
exit()
|
||
elif sys.argv[1] in ["-r", "--restart"]:
|
||
restartAccountSystemdServices(oldAccounts)
|
||
exit()
|
||
elif sys.argv[1] in ["-s", "--status"]:
|
||
getAccountServiceStates(oldAccounts)
|
||
exit()
|
||
elif sys.argv[1] in ["-h", "--help"]:
|
||
print("""\033[1;37mMbsync to IMAP Notify config generator.\033[0;37m
|
||
|
||
Usage: mbsync-imapnotify [options]
|
||
|
||
Options:
|
||
-e, --enable enable all services
|
||
-d, --disable disable all services
|
||
-r, --restart restart all services
|
||
-s, --status fetch the status for all services
|
||
-h, --help show this help
|
||
""", end='')
|
||
exit()
|
||
else:
|
||
print(f"\033[0;31mFlag {sys.argv[1]} not recognised, try --help\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/.config/imapnotify/%I/notify.conf
|
||
After=network.target
|
||
|
||
[Service]
|
||
ExecStart=%h/.local/bin/goimapnotify -conf %h/.config/imapnotify/%I/notify.conf
|
||
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
|
||
|
||
[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")
|
||
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")
|
||
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
|
||
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 //'
|
||
#+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")
|
||
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")
|
||
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.
|
||
|
||
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")
|
||
(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.
|
||
|
||
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.
|
||
|
||
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
|
||
#+begin_src emacs-lisp
|
||
(after! mu4e
|
||
(setq mu4e-headers-fields
|
||
'((:flags . 6)
|
||
(:account-stripe . 2)
|
||
(:from-or-to . 25)
|
||
(:folder . 10)
|
||
(:recipnum . 2)
|
||
(:subject . 80)
|
||
(:human-date . 8))
|
||
+mu4e-min-header-frame-width 142
|
||
mu4e-headers-date-format "%d/%m/%y"
|
||
mu4e-headers-time-format "⧖ %H:%M"
|
||
mu4e-headers-results-limit 1000
|
||
mu4e-index-cleanup t)
|
||
|
||
(add-to-list 'mu4e-bookmarks
|
||
'(:name "Yesterday's messages" :query "date:2d..1d" :key ?y) t)
|
||
|
||
(defvar +mu4e-header--folder-colors nil)
|
||
(appendq! mu4e-header-info-custom
|
||
'((:folder .
|
||
(:name "Folder" :shortname "Folder" :help "Lowest level folder" :function
|
||
(lambda (msg)
|
||
(+mu4e-colorize-str
|
||
(replace-regexp-in-string "\\`.*/" "" (mu4e-message-field msg :maildir))
|
||
'+mu4e-header--folder-colors)))))))
|
||
#+end_src
|
||
|
||
We'll also use a nicer alert icon
|
||
#+begin_src emacs-lisp
|
||
(setq mu4e-alert-icon "/usr/share/icons/Papirus/64x64/apps/evolution.svg")
|
||
#+end_src
|
||
**** Sending Mail
|
||
Let's send emails too.
|
||
#+begin_src emacs-lisp
|
||
(after! mu4e
|
||
(setq sendmail-program "/usr/bin/msmtp"
|
||
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
|
||
|
||
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.
|
||
|
||
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 :tangle (if (string= (shell-command-to-string "xdg-mime query default x-scheme-handler/mailto") "emacsmail.desktop\n") "no" "setup.sh")
|
||
update-desktop-database ~/.local/share/applications
|
||
#+end_src
|
||
|
||
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]].
|
||
|
||
It would also be nice to change the name pre-filled in =From:= when drafting.
|
||
#+begin_src emacs-lisp
|
||
(defvar mu4e-from-name "Timothy"
|
||
"Name used in \"From:\" template.")
|
||
(defadvice! mu4e~draft-from-construct-renamed (orig-fn)
|
||
"Wrap `mu4e~draft-from-construct-renamed' to change the name."
|
||
:around #'mu4e~draft-from-construct
|
||
(let ((user-full-name mu4e-from-name))
|
||
(funcall orig-fn)))
|
||
#+end_src
|
||
*** Org Msg
|
||
Doom does a fantastic stuff with the defaults with this, so we only make a few
|
||
minor tweaks.
|
||
#+begin_src emacs-lisp
|
||
(setq +org-msg-accent-color "#1a5fb4"
|
||
org-msg-greeting-fmt "\nHi %s,\n\n"
|
||
org-msg-signature "\n\n#+begin_signature\nAll the best,\\\\\n*Timothy*\n#+end_signature")
|
||
(map! :map org-msg-edit-mode-map
|
||
:after org-msg
|
||
:n "G" #'org-msg-goto-body)
|
||
#+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)
|
||
(set-file-template! "\\.org$" :trigger "__" :mode 'org-mode)
|
||
(set-file-template! "/LICEN[CS]E$" :trigger '+file-templates/insert-license)
|
||
#+end_src
|
||
** Plaintext
|
||
It's nice to see ANSI colour codes displayed
|
||
#+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
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: org
|
||
:header-args:emacs-lisp: :tangle no :noweb-ref org-conf
|
||
:END:
|
||
:intro:
|
||
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 |
|
||
|
||
#+attr_html: :class invertible :alt Radar chart comparing my opinions of document formats.
|
||
[[https://tecosaur.com/lfs/emacs-config/document-format-comparison.png]]
|
||
|
||
Beyond the elegance in the markup language, tremendously rich integrations with
|
||
Emacs allow for some fantastic [[https://orgmode.org/features.html][features]], such as what seems to be the best
|
||
support for [[https://en.wikipedia.org/wiki/Literate_programming][literate programming]] of any currently available technology.
|
||
|
||
#+name: Literate programming workflow
|
||
#+attr_html: :style line-height:1.13;
|
||
#+begin_example
|
||
╭─╴Code╶─╮ ╭─╴Raw Code╶─▶ Computer
|
||
Ideas╺┥ ┝━▶ Org Mode╺┥
|
||
╰─╴Text╶─╯ ╰─╴Document╶─▶ People
|
||
#+end_example
|
||
|
||
An =.org= file can contain blocks of code (with [[https://en.wikipedia.org/wiki/Noweb][noweb]] templating support), which
|
||
can be [[https://orgmode.org/manual/Extracting-Source-Code.html][tangled]] to dedicated source code files, and [[https://orgmode.org/manual/Extracting-Source-Code.html][woven]] into a document
|
||
(report, documentation, presentation, etc.) through various (/extensible/) methods.
|
||
These source blocks may even create images or other content to be included in
|
||
the document, or generate source code.
|
||
|
||
#+name: Example Org Flowchart
|
||
#+attr_html: :style line-height:1.13;
|
||
#+begin_example
|
||
╭───────────────────────────────────▶ .pdf ⎫
|
||
pdfLaTeX ▶╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╮ ⎪
|
||
╿ ╿ ┊ ⎪
|
||
│ ┊ ┊ ⎪
|
||
.tex ┊ ┊ ⎪
|
||
╿ ┊ ┊ ⎪
|
||
╭──┴╌╌╮ ┊ ┊ style.scss ⎬ Weaving
|
||
graphc.png ─╮ │ embedded TeX ┊ ╽ ⎪ (Documents)
|
||
image.jpeg ─┤ filters ╿ ┊ .css ⎪
|
||
╎ ╿ ┊ ┊ ▾╎ ⎪
|
||
figure.png╶─╧─▶ PROJECT.ORG ▶───╴filters╶───╧──────╪──▶ .html ⎪
|
||
╿ ╿┊ ║ │ ╰╌╌╌▷╌╌ embedded html ▶╌╌╌╌╯ ⎪
|
||
├╌╌╌╌╌╌╌▷╌╌╌╯┊ ║ │ ⎪
|
||
result╶╌╌╌╌╌╮ ┊ ║ ├──────╴filters╶────────────────▶ .txt ⎪
|
||
┊▴ ┊ ┊ ║ │ ⎪
|
||
execution ┊ ┊ ║ ╰──────╴filters╶────────────────▶ .md ⎭
|
||
┊▴ ┊ ┊ ║
|
||
code blocks◀╯ ┊ ╟─────────────────────────────────▶ .c ⎫
|
||
╰╌╌╌╌◁╌╌╌╌╌╌╌╯ ╟─────────────────────────────────▶ .sh ⎬ Tangling
|
||
╟─────────────────────────────────▶ .hs ⎪ (Code)
|
||
╙─────────────────────────────────▶ .el ⎭
|
||
#+end_example
|
||
:end:
|
||
|
||
Finally, because this section is fairly expensive to initialise, we'll wrap it
|
||
in an ~(after! ...)~ block.
|
||
#+begin_src emacs-lisp :noweb no-export :tangle yes :noweb-ref nil
|
||
(after! org
|
||
<<org-conf>>
|
||
)
|
||
#+end_src
|
||
*** System config
|
||
**** Mime types
|
||
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 :mkdirp yes :comments none
|
||
<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
|
||
#+begin_src shell :tangle (if (string= (shell-command-to-string "xdg-mime query default text/org") "") "setup.sh" "no")
|
||
update-mime-database ~/.local/share/mime
|
||
#+end_src
|
||
Then set Emacs as the default editor
|
||
#+begin_src shell :tangle (if (string= (shell-command-to-string "xdg-mime query default text/org") "emacs-client.desktop\n") "no" "setup.sh")
|
||
xdg-mime default emacs.desktop text/org
|
||
#+end_src
|
||
**** Development
|
||
Testing patches from the ML is currently more hassle than it needs to be. Let's
|
||
change that.
|
||
#+begin_src emacs-lisp
|
||
(defvar org-ml-target-dir "~/.emacs.d/.local/straight/repos/org-mode/")
|
||
(defvar org-ml-max-age 600
|
||
"Maximum permissible age in seconds.")
|
||
(defvar org-ml--cache-timestamp 0)
|
||
(defvar org-ml--cache nil)
|
||
|
||
(defun org-ml-current-patches ()
|
||
"Get the currently open patches, as a list of alists.
|
||
Entries of the form (subject . id)."
|
||
(delq nil
|
||
(mapcar
|
||
(lambda (entry)
|
||
(unless (plist-get entry :fixed)
|
||
(cons
|
||
(format "%-8s %s"
|
||
(propertize
|
||
(replace-regexp-in-string "T.*" ""
|
||
(plist-get entry :date))
|
||
'face 'font-lock-doc-face)
|
||
(propertize
|
||
(replace-regexp-in-string "\\[PATCH\\] ?" ""
|
||
(plist-get entry :summary))
|
||
'face 'font-lock-keyword-face))
|
||
(plist-get entry :id))))
|
||
(with-current-buffer (url-retrieve-synchronously "https://updates.orgmode.org/data/patches")
|
||
(json-parse-buffer :object-type 'plist)))))
|
||
|
||
(defun org-ml-select-patch-thread ()
|
||
"Find and apply a proposed Org patch."
|
||
(interactive)
|
||
(let ((current-workspace (+workspace-current))
|
||
(patches (progn
|
||
(when (or (not org-ml--cache)
|
||
(> (- (float-time) org-ml--cache-timestamp)
|
||
org-ml-max-age))
|
||
(setq org-ml--cache (org-ml-current-patches)
|
||
org-ml--cache-timestamp (float-time)))
|
||
org-ml--cache))
|
||
msg-id)
|
||
(ivy-read "Thread: "
|
||
patches
|
||
:action (lambda (m) (setq msg-id (cdr m))))
|
||
(+workspace-switch +mu4e-workspace-name)
|
||
(mu4e-view-message-with-message-id msg-id)
|
||
(add-to-list 'mu4e-view-actions
|
||
(cons "apply patch to org" #'org-ml-transient-mu4e-action))))
|
||
|
||
(defun org-ml-transient-mu4e-action (msg)
|
||
(setq mu4e-view-actions
|
||
(delete (cons "apply patch to org" #'org-ml-transient-mu4e-action)
|
||
mu4e-view-actions))
|
||
(+workspace/other)
|
||
(magit-status org-ml-target-dir)
|
||
(with-current-buffer (get-buffer-create "*Shell: Org apply patches*")
|
||
(erase-buffer)
|
||
(let ((default-directory org-ml-target-dir))
|
||
(shell-command
|
||
(format "git am %s"
|
||
(shell-quote-argument (mu4e-message-field msg :path)))
|
||
(current-buffer))
|
||
(magit-refresh))
|
||
(when (string-match-p "Error\\|failed" (buffer-string))
|
||
(+popup/buffer))))
|
||
#+end_src
|
||
*** Behaviour
|
||
[[xkcd:1319]]
|
||
**** 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-export-with-sub-superscripts '{} ; don't treat lone _ / ^ as sub/superscripts, require _{} / ^{}
|
||
org-re-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js")
|
||
#+end_src
|
||
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
|
||
|
||
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
|
||
**** Extra functionality
|
||
***** Org buffer creation
|
||
Let's also make creating an org buffer just that little bit easier.
|
||
#+begin_src emacs-lisp :tangle yes :noweb-ref none
|
||
(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
|
||
I think it makes sense to have list bullets change with depth
|
||
#+begin_src emacs-lisp
|
||
(setq org-list-demote-modify-bullet '(("+" . "-") ("-" . "+") ("*" . "+") ("1." . "a.")))
|
||
#+end_src
|
||
***** Citation
|
||
Occasionally I want to cite something.
|
||
#+begin_src emacs-lisp
|
||
(use-package! org-ref
|
||
: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
|
||
(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
|
||
(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
|
||
My spelling is atrocious, so let's get flycheck going.
|
||
#+begin_src emacs-lisp
|
||
(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
|
||
(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
|
||
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 no-export
|
||
(use-package! doct
|
||
:commands (doct))
|
||
|
||
(after! org-capture
|
||
<<prettify-capture>>
|
||
(setq +org-capture-uni-units (condition-case nil
|
||
(split-string (f-read-text "~/.org/.uni-units"))
|
||
(error nil))
|
||
+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))
|
||
|
||
(defun set-org-capture-templates ()
|
||
(setq org-capture-templates
|
||
(doct `(("Personal todo" :keys "t"
|
||
:icon ("checklist" :set "octicon" :color "green")
|
||
:file +org-capture-todo-file
|
||
:prepend t
|
||
:headline "Inbox"
|
||
:type entry
|
||
:template ("* TODO %?"
|
||
"%i %a")
|
||
)
|
||
("Personal note" :keys "n"
|
||
:icon ("sticky-note-o" :set "faicon" :color "green")
|
||
:file +org-capture-todo-file
|
||
:prepend t
|
||
:headline "Inbox"
|
||
:type entry
|
||
:template ("* %?"
|
||
"%i %a")
|
||
)
|
||
("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
|
||
: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}"
|
||
"%i %a")
|
||
:children (("General Task" :keys "k"
|
||
:icon ("inbox" :set "octicon" :color "yellow")
|
||
:extra ""
|
||
)
|
||
("Task with deadline" :keys "d"
|
||
:icon ("timer" :set "material" :color "orange" :v-adjust -0.1)
|
||
:extra "\nDEADLINE: %^{Deadline:}t"
|
||
)
|
||
("Scheduled Task" :keys "s"
|
||
:icon ("calendar" :set "octicon" :color "orange")
|
||
:extra "\nSCHEDULED: %^{Start time:}t"
|
||
)
|
||
))
|
||
("Project" :keys "p"
|
||
:icon ("repo" :set "octicon" :color "silver")
|
||
:prepend t
|
||
:type entry
|
||
:headline "Inbox"
|
||
:template ("* %{time-or-todo} %?"
|
||
"%i"
|
||
"%a")
|
||
:file ""
|
||
:custom (:time-or-todo "")
|
||
:children (("Project-local todo" :keys "t"
|
||
:icon ("checklist" :set "octicon" :color "green")
|
||
:time-or-todo "TODO"
|
||
:file +org-capture-project-todo-file)
|
||
("Project-local note" :keys "n"
|
||
:icon ("sticky-note" :set "faicon" :color "yellow")
|
||
:time-or-todo "%U"
|
||
:file +org-capture-project-notes-file)
|
||
("Project-local changelog" :keys "c"
|
||
:icon ("list" :set "faicon" :color "blue")
|
||
:time-or-todo "%U"
|
||
:heading "Unreleased"
|
||
:file +org-capture-project-changelog-file))
|
||
)
|
||
("\tCentralised project templates"
|
||
:keys "o"
|
||
:type entry
|
||
:prepend t
|
||
:template ("* %{time-or-todo} %?"
|
||
"%i"
|
||
"%a")
|
||
:children (("Project todo"
|
||
:keys "t"
|
||
:prepend nil
|
||
:time-or-todo "TODO"
|
||
:heading "Tasks"
|
||
:file +org-capture-central-project-todo-file)
|
||
("Project note"
|
||
:keys "n"
|
||
:time-or-todo "%U"
|
||
:heading "Notes"
|
||
:file +org-capture-central-project-notes-file)
|
||
("Project changelog"
|
||
:keys "c"
|
||
:time-or-todo "%U"
|
||
:heading "Unreleased"
|
||
:file +org-capture-central-project-changelog-file))
|
||
)))))
|
||
|
||
(set-org-capture-templates)
|
||
(unless (display-graphic-p)
|
||
(add-hook 'server-after-make-frame-hook
|
||
(defun org-capture-reinitialise-hook ()
|
||
(when (display-graphic-p)
|
||
(set-org-capture-templates)
|
||
(remove-hook 'server-after-make-frame-hook
|
||
#'org-capture-reinitialise-hook))))))
|
||
#+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
|
||
(not (pos-visible-in-window-p (1- (point-max)))))))
|
||
(setq current (concat current pressed))
|
||
(cond
|
||
((equal pressed "\C-g") (user-error "Abort"))
|
||
;; Selection is a prefix: open a new menu.
|
||
((member pressed des-keys))
|
||
;; Selection matches an association: return it.
|
||
((let ((entry (assoc current table)))
|
||
(and entry (throw 'exit entry))))
|
||
;; Selection matches a special entry: return the
|
||
;; selection prefix.
|
||
((assoc current specials) (throw 'exit current))
|
||
(t (error "No entry available")))))))
|
||
(when buffer (kill-buffer buffer))))))
|
||
(advice-add 'org-mks :override #'org-mks-pretty)
|
||
#+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
|
||
**** 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 :noweb-ref none :tangle yes
|
||
(setq org-roam-directory "~/Desktop/TEC/Organisation/Roam/")
|
||
#+end_src
|
||
|
||
That said, if the directory doesn't exist we likely don't want to be using roam.
|
||
Since we don't want to trigger errors (which will happen as soon as roam tries
|
||
to initialise), let's not load roam.
|
||
#+begin_src emacs-lisp :noweb-ref none :tangle (if (file-exists-p "~/Desktop/TEC/Organisation/Roam/") "no" "packages.el")
|
||
(package! org-roam :disable t)
|
||
#+end_src
|
||
***** Registering roam protocol
|
||
The recommended method of registering a protocol is by registering a desktop
|
||
application, which seems reasonable.
|
||
#+begin_src conf :tangle ~/.local/share/applications/org-protocol.desktop :mkdirp yes
|
||
[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 :tangle (if (string= (shell-command-to-string "xdg-mime query default x-scheme-handler/org-protocol") "org-protocol.desktop\n") "no" "setup.sh")
|
||
xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol
|
||
#+end_src
|
||
***** Graph Behaviour
|
||
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">
|
||
body {
|
||
background: white;
|
||
}
|
||
|
||
svg {
|
||
position: relative;
|
||
top: 50vh;
|
||
left: 50vw;
|
||
transform: translate(-50%, -50%);
|
||
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>
|
||
<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
|
||
(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
|
||
(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
|
||
`[: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")))
|
||
(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)))))))))
|
||
#+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--buffer-file-name-roam-aware-a (orig-fun)
|
||
:around #'doom-modeline-buffer-file-name ; takes no args
|
||
(if (s-contains-p org-roam-directory (or buffer-file-name ""))
|
||
(replace-regexp-in-string
|
||
"\\(?:^\\|.*/\\)\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)[0-9]*-"
|
||
"🢔(\\1-\\2-\\3) "
|
||
(subst-char-in-string ?_ ? buffer-file-name))
|
||
(funcall orig-fun)))
|
||
#+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-reference-contraction-max-words 3
|
||
"Maximum number of words in a reference reference.")
|
||
(defvar org-reference-contraction-max-length 35
|
||
"Maximum length of resulting reference reference, including joining characters.")
|
||
(defvar org-reference-contraction-stripped-words
|
||
'("the" "on" "in" "off" "a" "for" "by" "of" "and" "is" "to")
|
||
"Superfluous words to be removed from a reference.")
|
||
(defvar org-reference-contraction-joining-char "-"
|
||
"Character used to join words in the reference reference.")
|
||
|
||
(defun org-reference-contraction-truncate-words (words)
|
||
"Using `org-reference-contraction-max-length' as the total character 'budget' for the WORDS
|
||
and truncate individual words to conform to this budget.
|
||
|
||
To arrive at a budget that accounts for words undershooting their requisite average length,
|
||
the number of charachters in the budget freed by short words is distributed among the words
|
||
exceeding the average length. This adjusts the per-word budget to be the maximum feasable for
|
||
this particular situation, rather than the universal maximum average.
|
||
|
||
This budget-adjusted per-word maximum length is given by the mathematical expression below:
|
||
|
||
max length = \\floor{ \\frac{total length - chars for seperators - \\sum_{word \\leq average length} length(word) }{num(words) > average length} }"
|
||
;; trucate each word to a max word length determined by
|
||
;;
|
||
(let* ((total-length-budget (- org-reference-contraction-max-length ; how many non-separator chars we can use
|
||
(1- (length words))))
|
||
(word-length-budget (/ total-length-budget ; max length of each word to keep within budget
|
||
org-reference-contraction-max-words))
|
||
(num-overlong (-count (lambda (word) ; how many words exceed that budget
|
||
(> (length word) word-length-budget))
|
||
words))
|
||
(total-short-length (-sum (mapcar (lambda (word) ; total length of words under that budget
|
||
(if (<= (length word) word-length-budget)
|
||
(length word) 0))
|
||
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)))
|
||
words)))
|
||
|
||
(defun org-reference-contraction (reference-string)
|
||
"Give a contracted form of REFERENCE-STRING that is only contains alphanumeric characters.
|
||
Strips 'joining' words present in `org-reference-contraction-stripped-words',
|
||
and then limits the result to the first `org-reference-contraction-max-words' words.
|
||
If the total length is > `org-reference-contraction-max-length' then individual words are
|
||
truncated to fit within the limit using `org-reference-contraction-truncate-words'."
|
||
(let ((reference-words
|
||
(-filter (lambda (word)
|
||
(not (member word org-reference-contraction-stripped-words)))
|
||
(split-string
|
||
(->> reference-string
|
||
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 reference-words)
|
||
org-reference-contraction-max-words)
|
||
(setq reference-words
|
||
(cl-subseq reference-words 0 org-reference-contraction-max-words)))
|
||
|
||
(when (> (apply #'+ (1- (length reference-words))
|
||
(mapcar #'length reference-words))
|
||
org-reference-contraction-max-length)
|
||
(setq reference-words (org-reference-contraction-truncate-words reference-words)))
|
||
|
||
(string-join reference-words org-reference-contraction-joining-char)))
|
||
#+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)))
|
||
(unpackaged/org-export-html-with-useful-ids-mode 1) ; ensure enabled, and advice run
|
||
|
||
(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-reference-contraction (substring-no-properties title))
|
||
(unless (or headline-p (org-element-property :name datum))
|
||
(concat ","
|
||
(pcase (car datum)
|
||
('src-block "code")
|
||
('example "example")
|
||
('fixed-width "mono")
|
||
('property-drawer "properties")
|
||
(_ (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-reference-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
|
||
(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'.
|
||
(org-return t)))))
|
||
|
||
(map!
|
||
:after evil-org
|
||
:map evil-org-mode-map
|
||
:i [return] #'unpackaged/org-return-dwim)
|
||
#+end_src
|
||
**** Snippet Helpers
|
||
|
||
I often want to set =src-block= headers, and it's a pain to
|
||
+ type them out
|
||
+ remember what the accepted values are
|
||
+ oh, and specifying the same language again and again
|
||
|
||
We can solve this in three steps
|
||
+ having one-letter snippets, conditioned on ~(point)~ being within a src header
|
||
+ creating a nice prompt showing accepted values and the current default
|
||
+ pre-filling the =src-block= language with the last language used
|
||
|
||
For header args, the keys I'll use are
|
||
+ =r= for =:results=
|
||
+ =e= for =:exports=
|
||
+ =v= for =:eval=
|
||
+ =s= for =:session=
|
||
+ =d= for =:dir=
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun +yas/org-src-header-p ()
|
||
"Determine whether `point' is within a src-block header or header-args."
|
||
(pcase (org-element-type (org-element-context))
|
||
('src-block (< (point) ; before code part of the src-block
|
||
(save-excursion (goto-char (org-element-property :begin (org-element-context)))
|
||
(forward-line 1)
|
||
(point))))
|
||
('inline-src-block (< (point) ; before code part of the inline-src-block
|
||
(save-excursion (goto-char (org-element-property :begin (org-element-context)))
|
||
(search-forward "]{")
|
||
(point))))
|
||
('keyword (string-match-p "^header-args" (org-element-property :value (org-element-context))))))
|
||
#+end_src
|
||
|
||
Now let's write a function we can reference in yasnippets to produce a nice
|
||
interactive way to specify header args.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun +yas/org-prompt-header-arg (arg question values)
|
||
"Prompt the user to set ARG header property to one of VALUES with QUESTION.
|
||
The default value is identified and indicated. If either default is selected,
|
||
or no selection is made: nil is returned."
|
||
(let* ((src-block-p (not (looking-back "^#\\+property:[ \t]+header-args:.*" (line-beginning-position))))
|
||
(default
|
||
(or
|
||
(cdr (assoc arg
|
||
(if src-block-p
|
||
(nth 2 (org-babel-get-src-block-info t))
|
||
(org-babel-merge-params
|
||
org-babel-default-header-args
|
||
(let ((lang-headers
|
||
(intern (concat "org-babel-default-header-args:"
|
||
(+yas/org-src-lang)))))
|
||
(when (boundp lang-headers) (eval lang-headers t)))))))
|
||
""))
|
||
default-value)
|
||
(setq values (mapcar
|
||
(lambda (value)
|
||
(if (string-match-p (regexp-quote value) default)
|
||
(setq default-value
|
||
(concat value " "
|
||
(propertize "(default)" 'face 'font-lock-doc-face)))
|
||
value))
|
||
values))
|
||
(let ((selection (ivy-read question values :preselect default-value)))
|
||
(unless (or (string-match-p "(default)$" selection)
|
||
(string= "" selection))
|
||
selection))))
|
||
#+end_src
|
||
|
||
Finally, we fetch the language information for new source blocks.
|
||
|
||
Since we're getting this info, we might as well go a step further and also
|
||
provide the ability to determine the most popular language in the buffer that
|
||
doesn't have any =header-args= set for it (with =#+properties=).
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun +yas/org-src-lang ()
|
||
"Try to find the current language of the src/header at `point'.
|
||
Return nil otherwise."
|
||
(let ((context (org-element-context)))
|
||
(pcase (org-element-type context)
|
||
('src-block (org-element-property :language context))
|
||
('inline-src-block (org-element-property :language context))
|
||
('keyword (when (string-match "^header-args:\\([^ ]+\\)" (org-element-property :value context))
|
||
(match-string 1 (org-element-property :value context)))))))
|
||
|
||
(defun +yas/org-last-src-lang ()
|
||
"Return the language of the last src-block, if it exists."
|
||
(save-excursion
|
||
(beginning-of-line)
|
||
(when (re-search-backward "^[ ]*#\\+begin_src" nil t)
|
||
(org-element-property :language (org-element-context)))))
|
||
|
||
(defun +yas/org-most-common-no-property-lang ()
|
||
"Find the lang with the most source blocks that has no global header-args, else nil."
|
||
(let (src-langs header-langs)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(while (re-search-forward "^[ ]*#\\+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 (cl-set-difference src-langs header-langs :test #'string=))))
|
||
#+end_src
|
||
|
||
**** Translate capital keywords (old) to lower case (new)
|
||
Everyone used to use ~#+CAPITAL~ keywords. Then people realised that ~#+lowercase~
|
||
is actually both marginally easier and visually nicer, so now the capital
|
||
version is just used in the manual.
|
||
#+begin_quote
|
||
Org is standardized on lower case. Uppercase is used in the manual as a poor
|
||
man's bold, and supported for historical reasons. --- [[https://orgmode.org/list/87tuuw3n15.fsf@nicolasgoaziou.fr][Nicolas Goaziou on the Org ML]]
|
||
#+end_quote
|
||
|
||
To avoid sometimes having to choose between the hassle out of updating old
|
||
documents and using mixed syntax, I'll whip up a basic transcode-y function.
|
||
It likely misses some edge cases, but should mostly work.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun org-syntax-convert-case-to-lower ()
|
||
"Convert all #+KEYWORDS to #+keywords."
|
||
(interactive)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(let ((count 0)
|
||
(case-fold-search nil))
|
||
(while (re-search-forward "^[ ]*#\\+[A-Z_]+" nil t)
|
||
(replace-match (downcase (match-string 0)) t)
|
||
(setq count (1+ count)))
|
||
(message "Replaced %d occurances" count))))
|
||
#+end_src
|
||
**** 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
|
||
(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))
|
||
|
||
(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)))
|
||
(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)
|
||
"Convert xkcd to html/LaTeX form"
|
||
(let* ((xkcd-info (+xkcd-fetch-info (string-to-number num)))
|
||
(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))))
|
||
(cond ((org-export-derived-backend-p backend 'html)
|
||
(format "<img class='invertible' src='%s' title=\"%s\" alt='%s'>" img (subst-char-in-string ?\" ?“ alt) title))
|
||
((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)))))
|
||
|
||
(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"
|
||
(pcase 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))))
|
||
(_ (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)))
|
||
(pcase org-music-player
|
||
('mpris (org-music-mpris-play file start-time end-time))
|
||
(_ (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."
|
||
(pcase org-music-track-search-method
|
||
('file (org-music-find-file artist title))
|
||
('beets (org-music-beets-find-file artist title))
|
||
(_ (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"))))
|
||
|
||
(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
|
||
(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]]
|
||
|
||
**** 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
|
||
|
||
Let's make headings a bit bigger
|
||
#+begin_src emacs-lisp
|
||
(custom-set-faces!
|
||
'(outline-1 :weight extra-bold :height 1.25)
|
||
'(outline-2 :weight bold :height 1.15)
|
||
'(outline-3 :weight bold :height 1.12)
|
||
'(outline-4 :weight semi-bold :height 1.09)
|
||
'(outline-5 :weight semi-bold :height 1.06)
|
||
'(outline-6 :weight semi-bold :height 1.03)
|
||
'(outline-8 :weight semi-bold)
|
||
'(outline-9 :weight semi-bold))
|
||
#+end_src
|
||
|
||
And the same with the title.
|
||
#+begin_src emacs-lisp
|
||
(custom-set-faces!
|
||
'(org-document-title :height 1.2))
|
||
#+end_src
|
||
|
||
It seems reasonable to have deadlines in the error face when they're passed.
|
||
#+begin_src emacs-lisp
|
||
(setq org-agenda-deadline-faces
|
||
'((1.001 . error)
|
||
(1.0 . org-warning)
|
||
(0.5 . org-upcoming-deadline)
|
||
(0.0 . org-upcoming-distant-deadline)))
|
||
#+end_src
|
||
|
||
We can then have quote blocks stand out a bit more by making them /italic/.
|
||
#+begin_src emacs-lisp
|
||
(setq org-fontify-quote-and-verse-blocks t)
|
||
#+end_src
|
||
|
||
While ~org-hide-emphasis-markers~ is very nice, it can sometimes make edits which
|
||
occur at the border a bit more fiddley. We can improve this situation without
|
||
sacrificing visual amenities with the =org-appear= package.
|
||
#+begin_src emacs-lisp
|
||
(use-package! org-appear
|
||
:hook (org-mode . org-appear-mode)
|
||
:config
|
||
(setq org-appear-autoemphasis t
|
||
org-appear-autosubmarkers t
|
||
org-appear-autolinks nil)
|
||
;; for proper first-time setup, `org-appear--set-fragments'
|
||
;; needs to be run after other hooks have acted.
|
||
(run-at-time nil nil #'org-appear--set-fragments))
|
||
#+end_src
|
||
**** Symbols
|
||
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
|
||
;; (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 ))
|
||
|
||
(setq org-ellipsis " ▾ "
|
||
org-hide-leading-stars t
|
||
org-priority-highest ?A
|
||
org-priority-lowest ?E
|
||
org-priority-faces
|
||
'((?A . 'all-the-icons-red)
|
||
(?B . 'all-the-icons-orange)
|
||
(?C . 'all-the-icons-yellow)
|
||
(?D . 'all-the-icons-green)
|
||
(?E . 'all-the-icons-blue)))
|
||
#+end_src
|
||
It's also nice to make use of the Unicode characters for check boxes, and other commands.
|
||
#+begin_src emacs-lisp
|
||
(appendq! +ligatures-extra-symbols
|
||
`(:checkbox "☐"
|
||
:pending "◼"
|
||
:checkedbox "☑"
|
||
:list_property "∷"
|
||
:em_dash "—"
|
||
:ellipses "…"
|
||
:title "𝙏"
|
||
:subtitle "𝙩"
|
||
:author "𝘼"
|
||
:date "𝘿"
|
||
:property "☸"
|
||
:options "⌥"
|
||
:latex_class "🄲"
|
||
:latex_header "⇥"
|
||
:beamer_header "↠"
|
||
:attr_latex "🄛"
|
||
:attr_html "🄗"
|
||
:begin_quote "❮"
|
||
:end_quote "❯"
|
||
:caption "☰"
|
||
:header "›"
|
||
:results "🠶"
|
||
:begin_export "⏩"
|
||
:end_export "⏪"
|
||
:properties "⚙"
|
||
:end "∎"
|
||
: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)))
|
||
(set-ligatures! 'org-mode
|
||
:merge t
|
||
:checkbox "[ ]"
|
||
:pending "[-]"
|
||
:checkedbox "[X]"
|
||
:list_property "::"
|
||
:em_dash "---"
|
||
:ellipsis "..."
|
||
:title "#+title:"
|
||
:subtitle "#+subtitle:"
|
||
:author "#+author:"
|
||
:date "#+date:"
|
||
:property "#+property:"
|
||
:options "#+options:"
|
||
:latex_class "#+latex_class:"
|
||
:latex_header "#+latex_header:"
|
||
:beamer_header "#+beamer_header:"
|
||
:attr_latex "#+attr_latex:"
|
||
:attr_html "#+attr_latex:"
|
||
:begin_quote "#+begin_quote"
|
||
:end_quote "#+end_quote"
|
||
:caption "#+caption:"
|
||
:header "#+header:"
|
||
:begin_export "#+begin_export"
|
||
:end_export "#+end_export"
|
||
:results "#+RESULTS:"
|
||
:property ":PROPERTIES:"
|
||
:end ":END:"
|
||
:priority_a "[#A]"
|
||
:priority_b "[#B]"
|
||
:priority_c "[#C]"
|
||
:priority_d "[#D]"
|
||
:priority_e "[#E]")
|
||
(plist-put +ligatures-extra-symbols :name "⁍")
|
||
#+end_src
|
||
|
||
**** LaTeX Fragments
|
||
First off, we want those fragments to look good.
|
||
#+begin_src emacs-lisp
|
||
(setq org-highlight-latex-and-related '(native script entities))
|
||
#+end_src
|
||
|
||
What's better than syntax-highlighted LaTeX is /rendered/ LaTeX though, and we can
|
||
have this be performed automatically with =org-fragtog=.
|
||
#+begin_src emacs-lisp
|
||
(use-package! org-fragtog
|
||
:hook (org-mode . org-fragtog-mode))
|
||
#+end_src
|
||
|
||
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.
|
||
#+begin_src emacs-lisp
|
||
(setq org-format-latex-header "\\documentclass{article}
|
||
\\usepackage[usenames]{color}
|
||
|
||
\\usepackage[T1]{fontenc}
|
||
|
||
\\usepackage{booktabs}
|
||
|
||
\\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
|
||
\\usepackage[nofont,plaindd]{bmc-maths}
|
||
\\usepackage{arev}
|
||
")
|
||
#+end_src
|
||
|
||
We can either render from a ~dvi~ or ~pdf~ file, so let's benchmark ~latex~ and
|
||
~pdflatex~.
|
||
| ~latex~ time | ~pdflatex~ time |
|
||
|------------+---------------|
|
||
| 135 \pm 2 ms | 215 \pm 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 \pm 2 ms | 178 \pm 2 ms | 12 \pm 2 ms |
|
||
|
||
Now let's combine this to see what's best
|
||
| Tool chain | Total time | Resultant file size |
|
||
|--------------------+------------+---------------------|
|
||
| ~latex~ + ~dvipng~ | 226 \pm 2 ms | 7 KiB |
|
||
| ~latex~ + ~dvisvgm~ | 392 \pm 4 ms | 8 KiB |
|
||
| ~pdflatex~ + ~pdf2svg~ | 230 \pm 2 ms | 16 KiB |
|
||
|
||
So, let's use ~dvipng~ for previewing LaTeX fragments in-Emacs, but ~dvisvgm~ for [[LaTeX Rendering]].
|
||
/Unfortunately: it seems that svg sizing is annoying ATM, so let's actually not do this right now./
|
||
|
||
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.
|
||
#+begin_src emacs-lisp
|
||
;; 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")))
|
||
(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))))
|
||
#+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
|
||
(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
|
||
(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.
|
||
(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 (cl-loop for (begin . env) in
|
||
(org-element-map (org-element-parse-buffer) 'latex-environment
|
||
(lambda (env)
|
||
(cons
|
||
(org-element-property :begin env)
|
||
(org-element-property :value env))))
|
||
collect
|
||
(cond
|
||
((and (string-match "\\\\begin{equation}" env)
|
||
(not (string-match "\\\\tag{" env)))
|
||
(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
|
||
**** Org Plot
|
||
We can use some of the variables in =org-plot= to use the current doom theme
|
||
colours.
|
||
#+begin_src emacs-lisp
|
||
(after! org-plot
|
||
(defun org-plot/generate-theme (_type)
|
||
"Use the current Doom theme colours to generate a GnuPlot preamble."
|
||
(format "
|
||
fgt = \"textcolor rgb '%s'\" # foreground text
|
||
fgat = \"textcolor rgb '%s'\" # foreground alt text
|
||
fgl = \"linecolor rgb '%s'\" # foreground line
|
||
fgal = \"linecolor rgb '%s'\" # foreground alt line
|
||
|
||
# foreground colors
|
||
set border lc rgb '%s'
|
||
# change text colors of tics
|
||
set xtics @fgt
|
||
set ytics @fgt
|
||
# change text colors of labels
|
||
set title @fgt
|
||
set xlabel @fgt
|
||
set ylabel @fgt
|
||
# change a text color of key
|
||
set key @fgt
|
||
|
||
# line styles
|
||
set linetype 1 lw 2 lc rgb '%s' # red
|
||
set linetype 2 lw 2 lc rgb '%s' # blue
|
||
set linetype 3 lw 2 lc rgb '%s' # green
|
||
set linetype 4 lw 2 lc rgb '%s' # magenta
|
||
set linetype 5 lw 2 lc rgb '%s' # orange
|
||
set linetype 6 lw 2 lc rgb '%s' # yellow
|
||
set linetype 7 lw 2 lc rgb '%s' # teal
|
||
set linetype 8 lw 2 lc rgb '%s' # violet
|
||
|
||
# palette
|
||
set palette maxcolors 8
|
||
set palette defined ( 0 '%s',\
|
||
1 '%s',\
|
||
2 '%s',\
|
||
3 '%s',\
|
||
4 '%s',\
|
||
5 '%s',\
|
||
6 '%s',\
|
||
7 '%s' )
|
||
"
|
||
(doom-color 'fg)
|
||
(doom-color 'fg-alt)
|
||
(doom-color 'fg)
|
||
(doom-color 'fg-alt)
|
||
(doom-color 'fg)
|
||
;; colours
|
||
(doom-color 'red)
|
||
(doom-color 'blue)
|
||
(doom-color 'green)
|
||
(doom-color 'magenta)
|
||
(doom-color 'orange)
|
||
(doom-color 'yellow)
|
||
(doom-color 'teal)
|
||
(doom-color 'violet)
|
||
;; duplicated
|
||
(doom-color 'red)
|
||
(doom-color 'blue)
|
||
(doom-color 'green)
|
||
(doom-color 'magenta)
|
||
(doom-color 'orange)
|
||
(doom-color 'yellow)
|
||
(doom-color 'teal)
|
||
(doom-color 'violet)
|
||
))
|
||
(defun org-plot/gnuplot-term-properties (_type)
|
||
(format "background rgb '%s' size 1050,650"
|
||
(doom-color 'bg)))
|
||
(setq org-plot/gnuplot-script-preamble #'org-plot/generate-theme)
|
||
(setq org-plot/gnuplot-term-extra #'org-plot/gnuplot-term-properties))
|
||
#+end_src
|
||
*** Exporting
|
||
**** General settings
|
||
#+begin_src emacs-lisp
|
||
(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
|
||
(require 'ox-extra)
|
||
(ox-extras-activate '(ignore-headlines))
|
||
#+end_src
|
||
**** Acronym formatting
|
||
I like automatically using spaced small caps for acronyms. For strings I want to
|
||
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).
|
||
|
||
This has to be implemented on a per-format basis, currently HTML and LaTeX
|
||
exports are supported.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun org-export-filter-text-acronym (text backend _info)
|
||
"Wrap suspected acronyms in acronyms-specific formatting.
|
||
Treat sequences of 2+ capital letters (optionally succeeded by \"s\") as an acronym.
|
||
Ignore if preceeded by \";\" (for manual prevention) or \"\\\" (for LaTeX commands). "
|
||
(let ((base-backend
|
||
(cond
|
||
((org-export-derived-backend-p backend 'latex) 'latex)
|
||
;; Markdown is derived from HTML, but we don't want to format it
|
||
((org-export-derived-backend-p backend 'md) nil)
|
||
((org-export-derived-backend-p backend 'html) 'html)))
|
||
(case-fold-search nil))
|
||
(when base-backend
|
||
(replace-regexp-in-string
|
||
"[;\\\\]?\\b[A-Z][A-Z]+s?[^A-Z]"
|
||
(lambda (all-caps-str)
|
||
;; 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)
|
||
(t (let* ((trailing-s (when (equal (aref all-caps-str (- (length all-caps-str) 2)) ?s)
|
||
(pcase base-backend
|
||
('latex "\\protect\\scalebox{.91}[.84]{s}")
|
||
('html "<small>s</small>"))))
|
||
(acr (substring all-caps-str 0 (if trailing-s -2 -1)))
|
||
(final-char (substring all-caps-str -1)))
|
||
(pcase base-backend
|
||
('latex (concat "\\textls*[70]{\\textsc{" (s-downcase acr) "}" trailing-s "}" final-char))
|
||
('html (concat "<span class='acr'>" acr "</span>" trailing-s final-char)))))))
|
||
text t t))))
|
||
|
||
(add-to-list 'org-export-filter-plain-text-functions
|
||
#'org-export-filter-text-acronym)
|
||
|
||
;; We won't use `org-export-filter-headline-functions' because it
|
||
;; passes (and formats) the entire section contents. That's no good.
|
||
|
||
(defun org-html-format-headline-acronymised (todo todo-type priority text tags info)
|
||
"Like `org-html-format-headline-default-function', but with acronym formatting."
|
||
(org-html-format-headline-default-function
|
||
todo todo-type priority (org-export-filter-text-acronym text 'html info) tags info))
|
||
(setq org-html-format-headline-function #'org-html-format-headline-acronymised)
|
||
|
||
(defun org-latex-format-headline-acronymised (todo todo-type priority text tags info)
|
||
"Like `org-latex-format-headline-default-function', but with acronym formatting."
|
||
(org-latex-format-headline-default-function
|
||
todo todo-type priority (org-export-filter-text-acronym text 'latex info) tags info))
|
||
(setq org-latex-format-headline-function #'org-latex-format-headline-acronymised)
|
||
#+end_src
|
||
|
||
*** HTML Export
|
||
:PROPERTIES:
|
||
:header-args:emacs-lisp: :noweb-ref ox-html-conf
|
||
:END:
|
||
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-meta-tags #'org-html-meta-tags-fancy
|
||
org-html-checkbox-type 'html-span)
|
||
(setq org-html-style-default org-html-style-plain
|
||
org-html-meta-tags #'org-html-meta-tags-default
|
||
org-html-checkbox-type 'html)))
|
||
#+end_src
|
||
|
||
There are quite a few instances where I want to modify variables defined in
|
||
=ox-html=, so we'll wrap the contents of this section in an ~(after! ox-html ...)~
|
||
block.
|
||
#+begin_src emacs-lisp :noweb yes :noweb-ref org-conf
|
||
(after! ox-html
|
||
<<ox-html-conf>>
|
||
)
|
||
#+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 (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(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"
|
||
(org-export-data (plist-get info :date) info)
|
||
(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
|
||
|
||
I think it would be nice if "Table of Contents" brought you back to the top of
|
||
the page. Well, since we've done this much advising already...
|
||
#+begin_src emacs-lisp
|
||
(defadvice! org-html-toc-linked (depth info &optional scope)
|
||
"Build a table of contents.
|
||
|
||
Just like `org-html-toc', except the header is a link to \"#\".
|
||
|
||
DEPTH is an integer specifying the depth of the table. INFO is
|
||
a plist used as a communication channel. Optional argument SCOPE
|
||
is an element defining the scope of the table. Return the table
|
||
of contents as a string, or nil if it is empty."
|
||
:override #'org-html-toc
|
||
(let ((toc-entries
|
||
(mapcar (lambda (headline)
|
||
(cons (org-html--format-toc-headline headline info)
|
||
(org-export-get-relative-level headline info)))
|
||
(org-export-collect-headlines info depth scope))))
|
||
(when toc-entries
|
||
(let ((toc (concat "<div id=\"text-table-of-contents\">"
|
||
(org-html--toc-text toc-entries)
|
||
"</div>\n")))
|
||
(if scope toc
|
||
(let ((outer-tag (if (org-html--html5-fancy-p info)
|
||
"nav"
|
||
"div")))
|
||
(concat (format "<%s id=\"table-of-contents\">\n" outer-tag)
|
||
(let ((top-level (plist-get info :html-toplevel-hlevel)))
|
||
(format "<h%d><a href=\"#\" style=\"color:inherit; text-decoration: none;\">%s</a></h%d>\n"
|
||
top-level
|
||
(org-html--translate "Table of Contents" info)
|
||
top-level))
|
||
toc
|
||
(format "</%s>\n" outer-tag))))))))
|
||
#+end_src
|
||
|
||
Lastly, let's pile on some metadata. This gives my pages nice embeds.
|
||
#+begin_src emacs-lisp
|
||
(defun org-html-meta-tags-fancy (info)
|
||
"Use the INFO plist to construct the meta tags, as described in `org-html-meta-tags'."
|
||
(let ((title (org-html-plain-text
|
||
(org-element-interpret-data (plist-get info :title)) info))
|
||
(author (and (plist-get info :with-author)
|
||
(let ((auth (plist-get info :author)))
|
||
;; Return raw Org syntax.
|
||
(and auth (org-html-plain-text
|
||
(org-element-interpret-data auth) info))))))
|
||
(list
|
||
(when (org-string-nw-p author)
|
||
(list "name" "author" author))
|
||
(when (org-string-nw-p (plist-get info :description))
|
||
(list "name" "description"
|
||
(plist-get info :description)))
|
||
'("name" "generator" "org mode")
|
||
'("name" "theme-color" "#77aa99")
|
||
'("property" "og:type" "article")
|
||
(list "property" "og:title" title)
|
||
(let ((subtitle (org-export-data (plist-get info :subtitle) info)))
|
||
(when (org-string-nw-p subtitle)
|
||
(list "property" "og:description" subtitle)))
|
||
'("property" "og:image" "https://tecosaur.com/resources/org/nib.png")
|
||
'("property" "og:image:type" "image/png")
|
||
'("property" "og:image:width" "200")
|
||
'("property" "og:image:height" "200")
|
||
'("property" "og:image:alt" "Green fountain pen nib")
|
||
(when (org-string-nw-p author)
|
||
(list "property" "og:article:author:first_name" (car (s-split-up-to " " author 2))))
|
||
(when (and (org-string-nw-p author) (s-contains-p " " author))
|
||
(list "property" "og:article:author:last_name" (cadr (s-split-up-to " " author 2))))
|
||
(list "property" "og:article:published_time" (format-time-string "%FT%T%z")))))
|
||
|
||
(unless (functionp #'org-html-meta-tags-default)
|
||
(defalias 'org-html-meta-tags-default #'ignore))
|
||
(setq org-html-meta-tags #'org-html-meta-tags-fancy)
|
||
#+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" />
|
||
|
||
<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
|
||
(setq org-html-style-plain org-html-style-default
|
||
org-html-htmlize-output-type 'css
|
||
org-html-doctype "html5"
|
||
org-html-html5-fancy t)
|
||
|
||
(defun org-html-reload-fancy-style ()
|
||
(interactive)
|
||
(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/org-css/main.js" doom-private-dir))
|
||
"</script>\n<style>\n"
|
||
(f-read-text (expand-file-name "misc/org-css/main.css" doom-private-dir))
|
||
"</style>"))
|
||
(when org-fancy-html-export-mode
|
||
(setq org-html-style-default org-html-style-fancy)))
|
||
(org-html-reload-fancy-style)
|
||
#+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.
|
||
|
||
Since this collapsability seems useful to have on by default for certain chunks
|
||
of code, it would be nice if you could set it with =#+attr_html: :collapsed t=.
|
||
|
||
It would be nice to also have a corresponding global / session-local way of
|
||
setting this, but I haven't quite been able to get that working (yet).
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar org-html-export-collapsed nil)
|
||
(eval '(cl-pushnew '(:collapsed "COLLAPSED" "collapsed" org-html-export-collapsed t)
|
||
(org-export-backend-options (org-export-get-backend 'html))))
|
||
(add-to-list 'org-default-properties "EXPORT_COLLAPSED")
|
||
#+end_src
|
||
|
||
We can take our src block 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 (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(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))
|
||
(collapsed-p (member (or (org-export-read-attribute :attr_html src-block :collapsed)
|
||
(plist-get info :collapsed))
|
||
'("y" "yes" "t" t "true" "all"))))
|
||
(format
|
||
"<details id='%s' class='code'%s><summary%s>%s</summary>
|
||
<div class='gutter'>
|
||
<a href='#%s'>#</a>
|
||
<button title='Copy to clipboard' onclick='copyPreToClipbord(this)'>⎘</button>\
|
||
</div>
|
||
%s
|
||
</details>"
|
||
ref
|
||
(if collapsed-p "" " 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
|
||
|
||
#+name: Example, fixed width, and property blocks
|
||
#+begin_src emacs-lisp
|
||
(defun org-html-block-collapsable (orig-fn block contents info)
|
||
"Wrap the usual block in a <details>"
|
||
(if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(funcall orig-fn block contents info)
|
||
(let ((ref (org-export-get-reference block info))
|
||
(type (pcase (car block)
|
||
('property-drawer "Properties")))
|
||
(collapsed-default (pcase (car block)
|
||
('property-drawer t)
|
||
(_ nil)))
|
||
(collapsed-value (org-export-read-attribute :attr_html block :collapsed))
|
||
(collapsed-p (or (member (org-export-read-attribute :attr_html block :collapsed)
|
||
'("y" "yes" "t" t "true"))
|
||
(member (plist-get info :collapsed) '("all")))))
|
||
(format
|
||
"<details id='%s' class='code'%s>
|
||
<summary%s>%s</summary>
|
||
<div class='gutter'>\
|
||
<a href='#%s'>#</a>
|
||
<button title='Copy to clipboard' onclick='copyPreToClipbord(this)'>⎘</button>\
|
||
</div>
|
||
%s\n
|
||
</details>"
|
||
ref
|
||
(if (or collapsed-p 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
|
||
|
||
**** Include extra font-locking in htmlize
|
||
|
||
Org uses [[https://github.com/hniksic/emacs-htmlize][htmlize.el]] to export buffers with syntax highlighting.
|
||
|
||
The works fantastically, for the most part. Minor modes that provide
|
||
font-locking are /not/ loaded, and so do not impact the result.
|
||
|
||
By enabling these modes in ~htmlize-before-hook~ we can correct this behaviour.
|
||
|
||
#+begin_src emacs-lisp
|
||
(add-hook 'htmlize-before-hook #'highlight-numbers--turn-on)
|
||
#+end_src
|
||
|
||
**** 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~.
|
||
|
||
While we're at it, we can a link gutter, as we did with src blocks, and show the
|
||
~#+name~, if one is given.
|
||
|
||
#+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 (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(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'>
|
||
<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))))))
|
||
#+end_src
|
||
**** 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 (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(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)))))
|
||
#+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 (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress))
|
||
(funcall orig-fn toc-entries)
|
||
(replace-regexp-in-string "<input [^>]+><label [^>]+>\\(.+?\\)</label></li>" "\\1</li>"
|
||
(funcall orig-fn toc-entries))))
|
||
#+end_src
|
||
**** Make verbatim different to code
|
||
Since we have =verbatim= and ~code~, let's make use of the difference.
|
||
|
||
We can use ~code~ exclusively for code snippets and commands like: "calling ~(message
|
||
"Hello")~ in batch-mode Emacs prints to stdout like ~echo~".
|
||
Then we can use =verbatim= for miscellaneous 'other monospace' like keyboard
|
||
shortcuts: "either =C-c C-c= or =C-g= is likely the most useful keybinding in Emacs",
|
||
or file names: "I keep my configuration in =~/.config/doom/=", among other things.
|
||
|
||
Then, styling these two cases differently can help improve clarity in a document.
|
||
|
||
#+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
|
||
(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
|
||
I want to add GitHub-style links on hover for headings.
|
||
#+begin_src emacs-lisp
|
||
(defun org-export-html-headline-anchor (text backend info)
|
||
(when (and (org-export-derived-backend-p backend 'html)
|
||
org-fancy-html-export-mode)
|
||
(unless (bound-and-true-p org-msg-export-in-progress)
|
||
(replace-regexp-in-string
|
||
"<h\\([0-9]\\) id=\"\\([a-z0-9-]+\\)\">\\(.*[^ ]\\)<\\/h[0-9]>" ; this is quite restrictive, but due to `org-reference-contraction' I can do this
|
||
"<h\\1 id=\"\\2\">\\3<a aria-hidden=\"true\" href=\"#\\2\">#</a> </h\\1>"
|
||
text))))
|
||
|
||
(add-to-list 'org-export-filter-headline-functions
|
||
'org-export-html-headline-anchor)
|
||
#+end_src
|
||
**** LaTeX Rendering
|
||
***** Pre-rendered
|
||
I consider ~dvisvgm~ to be a rather compelling option. However this isn't scaled
|
||
very well at the moment.
|
||
#+begin_src emacs-lisp
|
||
;; (setq-default org-html-with-latex `dvisvgm)
|
||
#+end_src
|
||
|
||
***** 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
|
||
(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>
|
||
<script id=\"MathJax-script\" async
|
||
src=\"%PATH\"></script>")
|
||
#+end_src
|
||
*** LaTeX Export
|
||
**** Compiling
|
||
By default Org uses ~pdflatex~ \times 3 + ~bibtex~. This simply won't do in our
|
||
modern world. ~latexmk~ + ~biber~ (which is used automatically with ~latexmk~) is a
|
||
simply superior combination.
|
||
|
||
#+begin_src emacs-lisp
|
||
;; org-latex-compilers = ("pdflatex" "xelatex" "lualatex"), which are the possible values for %latex
|
||
(setq org-latex-pdf-process '("latexmk -%latex -shell-escape -interaction=nonstopmode -f -pdf -output-directory=%o %f"))
|
||
#+end_src
|
||
|
||
While ~org-latex-pdf-process~ does support a function, and we could use that
|
||
instead, this would no longer use the log buffer --- it's a bit blind, you give
|
||
it the file name and expect it to do its thing.
|
||
|
||
The default values of ~org-latex-compilers~ is given in commented form to see how
|
||
~org-latex-pdf-process~ works with them.
|
||
|
||
While the ~-%latex~ above is slightly hacky (~-pdflatex~ expects to be given a
|
||
value) it allows us to leave ~org-latex-compilers~ unmodified.
|
||
This is nice in case I open an org file that uses =#+LATEX_COMPILER= for example,
|
||
it should still work.
|
||
|
||
**** Nicer checkboxes
|
||
#+begin_src emacs-lisp
|
||
(defun +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
|
||
'+org-export-latex-fancy-item-checkboxes)
|
||
#+end_src
|
||
|
||
**** Class templates
|
||
We'll be setting up an nice preamble to use in a new default export class.
|
||
|
||
#+name: latex-fancy-preamble
|
||
#+begin_src LaTeX
|
||
\\usepackage[osf,largesc,helvratio=0.9]{newpxtext}
|
||
\\usepackage[scale=0.9]{sourcecodepro}
|
||
|
||
\\usepackage[activate={true,nocompatibility},final,tracking=true,kerning=true,spacing=true,factor=2000]{microtype}
|
||
|
||
\\setlength{\\parskip}{\\baselineskip}
|
||
\\setlength{\\parindent}{0pt}
|
||
|
||
\\AtBeginEnvironment{quote}{\\itshape}
|
||
#+end_src
|
||
|
||
The =hyperref= setup needs to be handled separately however.
|
||
#+name: latex-fancy-hyperref
|
||
#+begin_src LaTeX
|
||
\\colorlet{greenyblue}{blue!70!green}
|
||
\\colorlet{blueygreen}{blue!40!green}
|
||
\\providecolor{link}{named}{greenyblue}
|
||
\\providecolor{cite}{named}{blueygreen}
|
||
\\hypersetup{
|
||
pdfauthor={%a},
|
||
pdftitle={%t},
|
||
pdfkeywords={%k},
|
||
pdfsubject={%d},
|
||
pdfcreator={%c},
|
||
pdflang={%L},
|
||
breaklinks=true,
|
||
colorlinks=true,
|
||
linkcolor=,
|
||
urlcolor=link,
|
||
citecolor=cite\n}
|
||
\\urlstyle{same}
|
||
#+end_src
|
||
|
||
#+begin_src emacs-lisp :noweb no-export
|
||
(after! ox-latex
|
||
(add-to-list 'org-latex-classes
|
||
'("fancy-article"
|
||
"\\documentclass{scrartcl}\n
|
||
[DEFAULT-PACKAGES]
|
||
[PACKAGES]
|
||
|
||
<<latex-fancy-preamble>>
|
||
"
|
||
("\\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}")))
|
||
(add-to-list 'org-latex-classes
|
||
'("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}")))
|
||
(add-to-list 'org-latex-classes
|
||
'("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}"))))
|
||
|
||
(setq org-latex-default-class "fancy-article"
|
||
org-latex-tables-booktabs t
|
||
org-latex-hyperref-template "
|
||
<<latex-fancy-hyperref>>
|
||
")
|
||
#+end_src
|
||
|
||
**** A cleverer preamble
|
||
***** Use case
|
||
We always want some particular elements in the preamble, let's call this
|
||
the "universal preamble"
|
||
#+name: org-latex-universal-preamble
|
||
#+begin_src LaTeX
|
||
\\usepackage[main,include]{embedall}
|
||
\\IfFileExists{./\\jobname.org}{\\embedfile[desc=The original file]{\\jobname.org}}{}
|
||
#+end_src
|
||
|
||
We could have every package we could possibly need in every one of
|
||
~org-latex-classes~, but that's /horribly/ inefficient and I don't want to think
|
||
about maintaining that.
|
||
|
||
Instead, we can have a "universal preamble" which contains a snippet which we
|
||
want to /always/ appear, and then conditional preamble snippets, which are only
|
||
included when a certain regex is successfully found in the Org buffer.
|
||
|
||
***** Conditional Content
|
||
|
||
Let's consider some other content we only want in certain sutuations.
|
||
|
||
#+name: org-latex-caption-preamble
|
||
#+begin_src LaTeX
|
||
\\usepackage{subcaption}
|
||
\\usepackage[hypcap=true]{caption}
|
||
\\setkomafont{caption}{\\sffamily\\small}
|
||
\\setkomafont{captionlabel}{\\upshape\\bfseries}
|
||
\\captionsetup{justification=raggedright,singlelinecheck=true}
|
||
\\setcapindent{0pt}
|
||
\\usepackage{capt-of} % required by Org
|
||
#+end_src
|
||
|
||
#+name: org-latex-checkbox-preamble
|
||
#+begin_src LaTeX
|
||
\\usepackage{pifont}
|
||
\\newcommand{\\checkboxUnchecked}{$\\square$}
|
||
\\newcommand{\\checkboxTransitive}{\\rlap{\\raisebox{-0.1ex}{\\hspace{0.35ex}\\Large\\textbf -}}$\\square$}
|
||
\\newcommand{\\checkboxChecked}{\\rlap{\\raisebox{0.2ex}{\\hspace{0.35ex}\\scriptsize \\ding{52}}}$\\square$}
|
||
#+end_src
|
||
|
||
#+name: org-latex-box-preamble
|
||
#+begin_src LaTeX
|
||
% args = #1 Name, #2 Colour, #3 Ding, #4 Label
|
||
\\usepackage{pifont}
|
||
\\newcommand{\\defsimplebox}[4]{%
|
||
\\definecolor{#1}{HTML}{#2}
|
||
\\newenvironment{#1}
|
||
{%
|
||
\\vspace{-0.7\\baselineskip}%
|
||
\\textcolor{#1}{#3} \\textcolor{#1}{\\textbf{#4}}%
|
||
\\vspace{-0.8\\baselineskip}
|
||
\\begin{addmargin}[1em]{1em}
|
||
}{%
|
||
\\end{addmargin}
|
||
\\vspace{-0.5\\baselineskip}
|
||
}%
|
||
}
|
||
\\defsimplebox{warning}{e66100}{\\ding{68}}{Warning}
|
||
\\defsimplebox{info}{3584e4}{\\ding{68}}{Information}
|
||
\\defsimplebox{success}{26a269}{\\ding{68}}{\\vspace{-\\baselineskip}}
|
||
\\defsimplebox{error}{c01c28}{\\ding{68}}{Important}
|
||
#+end_src
|
||
|
||
Lastly, we will pass this content into some global variables we for ease of
|
||
access.
|
||
|
||
#+begin_src emacs-lisp :noweb no-export
|
||
(defvar org-latex-universal-preamble "
|
||
<<org-latex-universal-preamble>>
|
||
"
|
||
"Preamble to be included in every export.")
|
||
|
||
(defvar org-latex-caption-preamble "
|
||
<<org-latex-caption-preamble>>
|
||
"
|
||
"Preamble to be included to improve captions.")
|
||
|
||
(defvar org-latex-checkbox-preamble "
|
||
<<org-latex-checkbox-preamble>>
|
||
"
|
||
"Preamble to be included to improve checkboxes.")
|
||
|
||
(defvar org-latex-box-preamble "
|
||
<<org-latex-box-preamble>>
|
||
"
|
||
"Preamble to be included to improve boxes.")
|
||
#+end_src
|
||
|
||
***** Implementation
|
||
#+name: org-latex-conditional-preamble
|
||
#+begin_src emacs-lisp
|
||
(defvar org-latex-conditional-preambles
|
||
'((t . org-latex-universal-preamble)
|
||
("\\[\\[file:\\(?:[^\\]]+?|\\\\\\]\\)\\.svg\\]\\]" . "\\usepackage{svg}")
|
||
("\\[\\[file:\\(?:[^]]\\|\\\\\\]\\)+\\.\\(?:eps\\|pdf\\|png\\|jpeg\\|jpg\\|jbig2\\)\\]\\]" . "\\usepackage{graphicx}")
|
||
("^[ ]*|" . "\\usepackage{longtable}\n\\usepackage{booktabs}")
|
||
("\\\\(\\|\\\\\\[\\|\\\\begin{\\(?:math\\|displaymath\\|equation\\|align\\|flalign\\|multiline\\|gather\\)[a-z]*\\*?}"
|
||
. "\\usepackage{bmc-maths}")
|
||
("\\+[^ ].*[^ ]\\+\\|_[^ ].*[^ ]_\\|\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
|
||
. "\\usepackage[normalem]{ulem}")
|
||
(":float wrap" . "\\usepackage{wrapfig}")
|
||
(":float sideways" . "\\usepackage{rotating}")
|
||
("^[ ]*#\\+caption:\\|\\\\caption" . org-latex-caption-preamble)
|
||
("^[ ]*\\(?:[-+*]\\|[0-9]+[.)]\\|[A-Za-z]+[.)]\\) \\[[ -X]\\]" . org-latex-checkbox-preamble)
|
||
("^[ ]*#\\+begin_\\(?:warning\\|info\\|success\\|error\\)\\|\\\\begin{\\(?:warning\\|info\\|success\\|error\\)}"
|
||
. org-latex-box-preamble))
|
||
"Snippets which are conditionally included in the preamble of a LaTeX export.
|
||
|
||
Alist where when the car results in a non-nil value, the cdr is inserted in
|
||
the preamble. The car may be a:
|
||
- string, which is used as a regex search in the buffer
|
||
- symbol, the value of which used
|
||
- function, the result of the function is used
|
||
|
||
The cdr may be a:
|
||
- string, which is inserted without processing
|
||
- symbol, the value of which is inserted
|
||
- function, the result of which is inserted")
|
||
|
||
(defadvice! org-latex-header-smart-preamble (orig-fn tpl def-pkg pkg snippets-p &optional extra)
|
||
"Dynamically insert preamble content based on `org-latex-conditional-preambles'."
|
||
:around #'org-splice-latex-header
|
||
(let ((header (funcall orig-fn tpl def-pkg pkg snippets-p extra)))
|
||
(if snippets-p header
|
||
(concat header
|
||
(mapconcat (lambda (term-preamble)
|
||
(when (pcase (car term-preamble)
|
||
((pred stringp) (save-excursion
|
||
(goto-char (point-min))
|
||
(search-forward-regexp (car term-preamble) nil t)))
|
||
((pred functionp) (funcall (car term-preamble)))
|
||
((pred symbolp) (symbol-value (car term-preamble)))
|
||
(_ (user-error "org-latex-conditional-preambles key %s unable to be used" (car term-preamble))))
|
||
(concat
|
||
(pcase (cdr term-preamble)
|
||
((pred stringp) (cdr term-preamble))
|
||
((pred functionp) (funcall (cdr term-preamble)))
|
||
((pred symbolp) (symbol-value (cdr term-preamble)))
|
||
(_ (user-error "org-latex-conditional-preambles value %s unable to be used" (cdr term-preamble))))
|
||
"\n")))
|
||
org-latex-conditional-preambles
|
||
"") "\n"))))
|
||
#+end_src
|
||
|
||
***** Reduce default packages
|
||
|
||
Thanks to our additions, we can remove a few packages from
|
||
~org-latex-default-packages-alist~.
|
||
|
||
There are also some obsolete entries in the default value, specifically
|
||
+ =grffile='s capabilities are built into the current version of =graphicx=
|
||
+ =textcomp='s functionality has been included in LaTeX's core for a while now
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq org-latex-default-packages-alist
|
||
'(("AUTO" "inputenc" t ("pdflatex"))
|
||
("T1" "fontenc" t ("pdflatex"))
|
||
("" "xcolor" nil) ; Generally useful
|
||
("" "hyperref" nil)))
|
||
#+end_src
|
||
|
||
**** Pretty code blocks
|
||
|
||
We could just use minted for syntax highlighting --- however, we can do better!
|
||
The =engrave-faces= package lets us use Emacs' font-lock for syntax highlighting,
|
||
exporting that as LaTeX commands.
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq org-latex-listings 'engraved) ; NOTE non-standard value
|
||
#+end_src
|
||
|
||
Thanks to ~org-latex-conditional-preambles~ and some copy-paste with the =minted=
|
||
entry in ~org-latex-scr-block~ we can easily add this as a recognised
|
||
~org-latex-listings~ value.
|
||
|
||
#+begin_src emacs-lisp :noweb no-export
|
||
(defadvice! org-latex-src-block-engraved (orig-fn src-block contents info)
|
||
"Like `org-latex-src-block', but supporting an engraved backend"
|
||
:around #'org-latex-src-block
|
||
(if (eq 'engraved (plist-get info :latex-listings))
|
||
(org-latex-scr-block--engraved src-block contents info)
|
||
(funcall orig-fn src-block contents info)))
|
||
|
||
(defadvice! org-latex-inline-src-block-engraved (orig-fn inline-src-block contents info)
|
||
"Like `org-latex-inline-src-block', but supporting an engraved backend"
|
||
:around #'org-latex-inline-src-block
|
||
(if (eq 'engraved (plist-get info :latex-listings))
|
||
(org-latex-inline-scr-block--engraved inline-src-block contents info)
|
||
(funcall orig-fn src-block contents info)))
|
||
|
||
(setq org-latex-engraved-code-preamble "
|
||
<<org-latex-engraved-code-preamble>>
|
||
")
|
||
|
||
(add-to-list 'org-latex-conditional-preambles '("^[ ]*#\\+BEGIN_SRC\\|#\\+begin_src" . org-latex-engraved-code-preamble) t)
|
||
(add-to-list 'org-latex-conditional-preambles '("^[ ]*#\\+BEGIN_SRC\\|#\\+begin_src" . engrave-faces-latex-gen-preamble) t)
|
||
|
||
(defun org-latex-scr-block--engraved (src-block contents info)
|
||
(let* ((lang (org-element-property :language src-block))
|
||
(attributes (org-export-read-attribute :attr_latex src-block))
|
||
(float (plist-get attributes :float))
|
||
(num-start (org-export-get-loc src-block info))
|
||
(retain-labels (org-element-property :retain-labels src-block))
|
||
(caption (org-element-property :caption src-block))
|
||
(caption-above-p (org-latex--caption-above-p src-block info))
|
||
(caption-str (org-latex--caption/label-string src-block info))
|
||
(placement (or (org-unbracket-string "[" "]" (plist-get attributes :placement))
|
||
(plist-get info :latex-default-figure-position)))
|
||
(float-env
|
||
(cond
|
||
((string= "multicolumn" float)
|
||
(format "\\begin{listing*}[%s]\n%s%%s\n%s\\end{listing*}"
|
||
placement
|
||
(if caption-above-p caption-str "")
|
||
(if caption-above-p "" caption-str)))
|
||
(caption
|
||
(format "\\begin{listing}[%s]\n%s%%s\n%s\\end{listing}"
|
||
placement
|
||
(if caption-above-p caption-str "")
|
||
(if caption-above-p "" caption-str)))
|
||
((string= "t" float)
|
||
(concat (format "\\begin{listing}[%s]\n"
|
||
placement)
|
||
"%s\n\\end{listing}"))
|
||
(t "%s")))
|
||
(options (plist-get info :latex-minted-options))
|
||
(content-buffer
|
||
(with-temp-buffer
|
||
(insert
|
||
(let* ((code-info (org-export-unravel-code src-block))
|
||
(max-width
|
||
(apply 'max
|
||
(mapcar 'length
|
||
(org-split-string (car code-info)
|
||
"\n")))))
|
||
(org-export-format-code
|
||
(car code-info)
|
||
(lambda (loc _num ref)
|
||
(concat
|
||
loc
|
||
(when ref
|
||
;; Ensure references are flushed to the right,
|
||
;; separated with 6 spaces from the widest line
|
||
;; of code.
|
||
(concat (make-string (+ (- max-width (length loc)) 6)
|
||
?\s)
|
||
(format "(%s)" ref)))))
|
||
nil (and retain-labels (cdr code-info)))))
|
||
(funcall (org-src-get-lang-mode lang))
|
||
(engrave-faces-latex-buffer)))
|
||
(content
|
||
(with-current-buffer content-buffer
|
||
(buffer-string)))
|
||
(body
|
||
(format
|
||
"\\begin{Code}\n\\begin{Verbatim}[%s]\n%s\\end{Verbatim}\n\\end{Code}"
|
||
;; Options.
|
||
(concat
|
||
(org-latex--make-option-string
|
||
(if (or (not num-start) (assoc "linenos" options))
|
||
options
|
||
(append
|
||
`(("linenos")
|
||
("firstnumber" ,(number-to-string (1+ num-start))))
|
||
options)))
|
||
(let ((local-options (plist-get attributes :options)))
|
||
(and local-options (concat "," local-options))))
|
||
content)))
|
||
(kill-buffer content-buffer)
|
||
;; Return value.
|
||
(format float-env body)))
|
||
|
||
(defun org-latex-inline-scr-block--engraved (inline-src-block _contents info)
|
||
(let ((options (org-latex--make-option-string
|
||
(plist-get info :latex-minted-options)))
|
||
code-buffer code)
|
||
(setq code-buffer
|
||
(with-temp-buffer
|
||
(insert (org-element-property :value inline-src-block))
|
||
(funcall (org-src-get-lang-mode
|
||
(org-element-property :language inline-src-block)))
|
||
(engrave-faces-latex-buffer)))
|
||
(setq code (with-current-buffer code-buffer
|
||
(buffer-string)))
|
||
(kill-buffer code-buffer)
|
||
(format "\\Verb%s{%s}"
|
||
(if (string= options "") ""
|
||
(format "[%s]" options))
|
||
code)))
|
||
#+end_src
|
||
|
||
Whenever this is used, in order for it to actually work (and look a little
|
||
better) we add bit to the preamble:
|
||
|
||
#+name: org-latex-engraved-code-preamble
|
||
#+begin_src LaTeX
|
||
\\usepackage{fvextra}
|
||
\\fvset{
|
||
commandchars=\\\\\\{\\},
|
||
highlightcolor=white!95!black!80!blue,
|
||
breaklines=true,
|
||
breaksymbol=\\color{white!60!black}\\tiny\\ensuremath{\\hookrightarrow}}
|
||
\\renewcommand\\theFancyVerbLine{\\footnotesize\\color{black!40!white}\\arabic{FancyVerbLine}}
|
||
|
||
% TODO have code boxes keep line vertical alignment
|
||
\\usepackage[breakable,xparse]{tcolorbox}
|
||
\\DeclareTColorBox[]{Code}{o}%
|
||
{colback=white!97!black, colframe=white!94!black,
|
||
fontupper=\\color{EFD}\\footnotesize,
|
||
IfNoValueTF={#1}%
|
||
{boxsep=2pt, arc=2.5pt, outer arc=2.5pt,
|
||
boxrule=0.5pt, left=2pt}%
|
||
{boxsep=2.5pt, arc=0pt, outer arc=0pt,
|
||
boxrule=0pt, leftrule=1.5pt, left=0.5pt},
|
||
right=2pt, top=1pt, bottom=0.5pt,
|
||
breakable}
|
||
#+end_src
|
||
|
||
At some point it would be nice to make the box colours easily customisable. At
|
||
the moment it's fairly easy to change the syntax highlighting colours with
|
||
src_emacs-lisp[:eval no :exports code]{(setq engrave-faces-preset-styles
|
||
(engrave-faces-generate-preset))}, but perhaps a toggle which specifies whether
|
||
to use the default values, the current theme, or any named theme could be a good
|
||
idea. It should also possible to set the box background dynamically to match.
|
||
The named theme could work by looking for a style definition with a certain name
|
||
in a cache dir, and then switching to that theme and producing (and saving) the
|
||
style definition if it doesn't exist.
|
||
|
||
Now let's have the example block be styled similarly.
|
||
#+begin_src emacs-lisp
|
||
(defadvice! org-latex-example-block-engraved (orig-fn example-block contents info)
|
||
"Like `org-latex-example-block', but supporting an engraved backend"
|
||
:around #'org-latex-example-block
|
||
(let ((output-block (funcall orig-fn example-block contents info)))
|
||
(if (eq 'engraved (plist-get info :latex-listings))
|
||
(format "\\begin{Code}[alt]\n%s\n\\end{Code}" output-block)
|
||
output-block)))
|
||
#+end_src
|
||
|
||
In addition to the vastly superior visual output, this should also be much
|
||
faster to for code-heavy documents (like this config).
|
||
|
||
Performing a little benchmark with this document, I find that this is indeed the
|
||
case.
|
||
|
||
| LaTeX syntax highlighting backend | Compile time | Overhead | Overhead ratio |
|
||
|-----------------------------------+--------------+----------+----------------|
|
||
| verbatim | 12 s | 0 | 0.0 |
|
||
| lstlistings | 15 s | 3 s | 0.2 |
|
||
| Engrave | 34 s | 22 s | 1.8 |
|
||
| Pygments (Minted) | 184 s | 172 s | 14.3 |
|
||
#+TBLFM: $3=$2-@2$2::$4=$3 / @2$2;%.1f
|
||
|
||
Treating the verbatim (no syntax highlighting) result as a baseline; this
|
||
rudimentary test suggest that =engrave-faces= is around eight times faster than
|
||
=pygments=, and takes three times as long as no syntax highlighting (verbatim).
|
||
|
||
**** Remove non-ascii chars
|
||
|
||
When using ~pdflatex~, almost non-ascii characters are generally problematic, and
|
||
don't appear in the pdf. It's preferable to see that there was /some/ character
|
||
which wasn't displayed as opposed to nothing.
|
||
|
||
So, as a basic first-pass we replace every non-ascii char with =¿=. In future I
|
||
could add sensible replacements (e.g. turn =§= into =\S=, and =…= with =\ldots=).
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun +org-latex-replace-non-ascii-chars (text backend info)
|
||
"Replace non-ascii chars with \\char\"XYZ forms."
|
||
(when (and (org-export-derived-backend-p backend 'latex)
|
||
(string= (plist-get info :latex-compiler) "pdflatex"))
|
||
(replace-regexp-in-string "[^[:ascii:]]" "¿" text)))
|
||
|
||
(add-to-list 'org-export-filter-final-output-functions #'+org-latex-replace-non-ascii-chars)
|
||
#+end_src
|
||
|
||
**** Support images from URLs
|
||
You can link to remote images easily, and they work nicely with HTML-based
|
||
exports. However, LaTeX can only include local files, and so the current
|
||
behaviour of =org-latex-link= is just to insert a URL to the image.
|
||
|
||
We can do better than that by downloading the image to a predictable location,
|
||
and using that. By making the filename predictable as opposed to just another
|
||
tempfile, this can provide a caching mechanism.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defadvice! +org-latex-link (orig-fn link desc info)
|
||
"Acts as `org-latex-link', but supports remote images."
|
||
:around #'org-latex-link
|
||
(setq o-link link
|
||
o-desc desc
|
||
o-info info)
|
||
(if (and (member (plist-get (cadr link) :type) '("http" "https"))
|
||
(member (file-name-extension (plist-get (cadr link) :path))
|
||
'("png" "jpg" "jpeg" "pdf" "svg")))
|
||
(org-latex-link--remote link desc info)
|
||
(funcall orig-fn link desc info)))
|
||
|
||
(defun org-latex-link--remote (link _desc info)
|
||
(let* ((url (plist-get (cadr link) :raw-link))
|
||
(ext (file-name-extension url))
|
||
(target (format "%s%s.%s"
|
||
(temporary-file-directory)
|
||
(replace-regexp-in-string "[./]" "-"
|
||
(file-name-sans-extension (substring (plist-get (cadr link) :path) 2)))
|
||
ext)))
|
||
(unless (file-exists-p target)
|
||
(url-copy-file url target))
|
||
(setcdr link (--> (cadr link)
|
||
(plist-put it :type "file")
|
||
(plist-put it :path target)
|
||
(plist-put it :raw-link (concat "file:" target))
|
||
(list it)))
|
||
(concat "% fetched from " url "\n"
|
||
(org-latex--inline-image link info))))
|
||
#+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).
|
||
|
||
This gives the same advantages as mentioned in the [[*Make verbatim different to code][HTML export section]].
|
||
|
||
#+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
|
||
*** Beamer Export
|
||
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)
|
||
#+end_src
|
||
*** Markdown Export
|
||
When I want to paste exported markdown somewhere (for example when using [[Emacs Everywhere][Emacs
|
||
Everywhere]]), it can be preferable to have unicode characters for =---= etc. instead
|
||
of =—=.
|
||
|
||
To accomplish this, we just need to locally rebind the alist which provides
|
||
these substitution.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defadvice! org-md-plain-text-unicode-a (orig-fn text info)
|
||
"Locally rebind `org-html-special-string-regexps'"
|
||
:around #'org-md-plain-text
|
||
(let ((org-html-special-string-regexps
|
||
'(("\\\\-" . "-")
|
||
("---\\([^-]\\)" . "—\\1")
|
||
("--\\([^-]\\)" . "–\\1")
|
||
("\\.\\.\\." . "…"))))
|
||
(funcall orig-fn text (plist-put info :with-smart-quotes nil))))
|
||
#+end_src
|
||
|
||
In the future, I may want to check =info= to only have this active when =ox-gfm= is
|
||
being used.
|
||
|
||
Another worthwhile consideration is LaTeX formatting. It seems most Markdown
|
||
parsers are fixated on TeX-style syntax (=$= and =$$=). As unfortunate as this is,
|
||
it's probably best to accommodate them, for the sake of decent rendering.
|
||
=ox-md= doesn't provide any transcoders for this, so we'll have to whip up our own
|
||
and push them onto the =md= transcoders alist.
|
||
|
||
#+begin_src emacs-lisp
|
||
(after! ox-md
|
||
(defun org-md-latex-fragment (latex-fragment _contents info)
|
||
"Transcode a LATEX-FRAGMENT object from Org to Markdown."
|
||
(let ((frag (org-element-property :value latex-fragment)))
|
||
(cond
|
||
((string-match-p "^\\\\(" frag)
|
||
(concat "$" (substring frag 2 -2) "$"))
|
||
((string-match-p "^\\\\\\[" frag)
|
||
(concat "$$" (substring frag 2 -2) "$$"))
|
||
(t (message "unrecognised fragment: %s" frag)
|
||
frag))))
|
||
|
||
(defun org-md-latex-environment (latex-environment contents info)
|
||
"Transcode a LATEX-ENVIRONMENT object from Org to Markdown."
|
||
(concat "$$\n"
|
||
(org-html-latex-environment latex-environment contents info)
|
||
"$$\n"))
|
||
|
||
(defun org-utf8-entity (entity _contents _info)
|
||
"Transcode an ENTITY object from Org to utf-8.
|
||
CONTENTS are the definition itself. INFO is a plist holding
|
||
contextual information."
|
||
(org-element-property :utf-8 entity))
|
||
|
||
;; We can't let this be immediately parsed and evaluated,
|
||
;; because eager macro-expansion tries to call as-of-yet
|
||
;; undefined functions.
|
||
;; NOTE in the near future this shouldn't be required
|
||
(eval
|
||
'(dolist (extra-transcoder
|
||
'((latex-fragment . org-md-latex-fragment)
|
||
(latex-environment . org-md-latex-environment)
|
||
(entity . org-utf8-entity)))
|
||
(unless (member extra-transcoder (org-export-backend-transcoders
|
||
(org-export-get-backend 'md)))
|
||
(push extra-transcoder (org-export-backend-transcoders
|
||
(org-export-get-backend 'md)))))))
|
||
#+end_src
|
||
|
||
*** Babel
|
||
Doom lazy-loads babel languages, with is lovely.
|
||
It also pulls in [[https://github.com/astahlman/ob-async][ob-async]], which is nice, but it would be even better if it was
|
||
used by default.
|
||
|
||
There are two caveats to =ob-async=:
|
||
1. It does not support =:session= + So, we don't want =:async= used when =:session= is set
|
||
2. It adds a fixed delay to execution
|
||
+ This is undesirable in a number of cases, for example it's generally
|
||
unwanted with =emacs-lisp= code
|
||
+ As such, I also introduce a async language blacklist to control when it's
|
||
automatically enabled
|
||
|
||
Due to the nuance in the desired behaviour, instead of just adding =:async= to
|
||
~org-babel-default-header-args~, I advice ~org-babel-get-src-block-info~ to add
|
||
=:async= intelligently. As an escape hatch, it also recognises =:sync= as an
|
||
indication that =:async= should not be added.
|
||
|
||
#+begin_src emacs-lisp
|
||
(add-transient-hook! #'org-babel-execute-src-block
|
||
(require 'ob-async))
|
||
|
||
(defvar org-babel-async-language-blacklist '("emacs-lisp" "latex" "LaTeX")
|
||
"Babel languages which should not be executed asyncronously.")
|
||
|
||
(defadvice! org-babel-get-src-block-info-eager-async-a (orig-fn &optional light datum)
|
||
"Eagarly add an :async parameter to the src information, unless it seems problematic.
|
||
Not added when either:
|
||
+ session is not \"none\"
|
||
+ :sync is set
|
||
+ the language is in `org-babel-async-language-blacklist'"
|
||
:around #'org-babel-get-src-block-info
|
||
(let ((result (funcall orig-fn light datum)))
|
||
(unless (or (not (string= "none" (cdr (assoc :session (caddr result)))))
|
||
(member (car result) org-babel-async-language-blacklist)
|
||
(assoc :async (caddr result)) ; don't duplicate
|
||
(assoc :sync (caddr result)))
|
||
(push '(:async) (caddr result)))
|
||
result))
|
||
#+end_src
|
||
*** ESS
|
||
We don't want ~R~ evaluation to hang the editor, hence
|
||
#+begin_src emacs-lisp
|
||
(setq ess-eval-visibly 'nowait)
|
||
#+end_src
|
||
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]]
|
||
*** 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)
|
||
#+begin_src shell :eval no :tangle no
|
||
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?)
|
||
*** 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
|
||
|
||
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-preamble
|
||
#+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.9]{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 no-export
|
||
(setq tec/yas-latex-template-preamble "
|
||
<<latex-nice-preamble>>
|
||
")
|
||
|
||
(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)))
|
||
(pcase open-char
|
||
(?\( "(")
|
||
(?9 "(")
|
||
(?\[ "[")
|
||
(?\{ "\\{")
|
||
(?< "<")
|
||
(?| (if tec/tex-delim-dot-second "." "|"))
|
||
(_ ".")))
|
||
(defun tec/tex-close-delim-from-char (&optional open-char)
|
||
"Find the associated closing delim as string"
|
||
(if tec/tex-delim-dot-second
|
||
(pcase tec/tex-delim-dot-second
|
||
(?\) ")")
|
||
(?0 ")")
|
||
(?\] "]")
|
||
(?\} "\\}")
|
||
(?\> ">")
|
||
(?| "|")
|
||
(_ "."))
|
||
(pcase (or open-char tec/tex-last-delim-char)
|
||
(?\( ")")
|
||
(?9 ")")
|
||
(?\[ "]")
|
||
(?\{ "\\}")
|
||
(?< ">")
|
||
(?\) ")")
|
||
(?0 ")")
|
||
(?\] "]")
|
||
(?\} "\\}")
|
||
(?\> ">")
|
||
(?| "|")
|
||
(_ "."))))
|
||
(defun tec/tex-next-char-smart-close-delim (&optional open-char)
|
||
(and (bound-and-true-p smartparens-mode)
|
||
(eql (char-after) (pcase (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
|
||
*** Editor visuals
|
||
Once again, /all hail mixed pitch mode!/
|
||
#+begin_src emacs-lisp
|
||
(add-hook 'LaTeX-mode-hook #'mixed-pitch-mode)
|
||
#+end_src
|
||
|
||
Let's enhance ~TeX-fold-math~ a bit
|
||
#+begin_src emacs-lisp
|
||
(after! latex
|
||
(setcar (assoc "⋆" LaTeX-fold-math-spec-list) "★")) ;; make \star bigger
|
||
|
||
(setq TeX-fold-math-spec-list
|
||
`(;; missing/better symbols
|
||
("≤" ("le"))
|
||
("≥" ("ge"))
|
||
("≠" ("ne"))
|
||
;; conviniance shorts -- these don't work nicely ATM
|
||
;; ("‹" ("left"))
|
||
;; ("›" ("right"))
|
||
;; private macros
|
||
("ℝ" ("RR"))
|
||
("ℕ" ("NN"))
|
||
("ℤ" ("ZZ"))
|
||
("ℚ" ("QQ"))
|
||
("ℂ" ("CC"))
|
||
("ℙ" ("PP"))
|
||
("ℍ" ("HH"))
|
||
("𝔼" ("EE"))
|
||
("𝑑" ("dd"))
|
||
;; known commands
|
||
("" ("phantom"))
|
||
(,(lambda (num den) (if (and (TeX-string-single-token-p num) (TeX-string-single-token-p den))
|
||
(concat num "/" den)
|
||
(concat "❪" num "/" den "❫"))) ("frac"))
|
||
(,(lambda (arg) (concat "√" (TeX-fold-parenthesize-as-neccesary arg))) ("sqrt"))
|
||
(,(lambda (arg) (concat "⭡" (TeX-fold-parenthesize-as-neccesary arg))) ("vec"))
|
||
("‘{1}’" ("text"))
|
||
;; private commands
|
||
("|{1}|" ("abs"))
|
||
("‖{1}‖" ("norm"))
|
||
("⌊{1}⌋" ("floor"))
|
||
("⌈{1}⌉" ("ceil"))
|
||
("⌊{1}⌉" ("round"))
|
||
("𝑑{1}/𝑑{2}" ("dv"))
|
||
("∂{1}/∂{2}" ("pdv"))
|
||
;; fancification
|
||
("{1}" ("mathrm"))
|
||
(,(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"))
|
||
)
|
||
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"))
|
||
))
|
||
|
||
(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)
|
||
(string-offset-apply-roman-char-exceptions
|
||
(+ (if (>= c 97) (- c 6) c) offset)))
|
||
word)))
|
||
|
||
(defvar string-offset-roman-char-exceptions
|
||
'(;; lowercase serif
|
||
(119892 . 8462) ; ℎ
|
||
;; lowercase caligraphic
|
||
(119994 . 8495) ; ℯ
|
||
(119996 . 8458) ; ℊ
|
||
(120004 . 8500) ; ℴ
|
||
;; caligraphic
|
||
(119965 . 8492) ; ℬ
|
||
(119968 . 8496) ; ℰ
|
||
(119969 . 8497) ; ℱ
|
||
(119971 . 8459) ; ℋ
|
||
(119972 . 8464) ; ℐ
|
||
(119975 . 8466) ; ℒ
|
||
(119976 . 8499) ; ℳ
|
||
(119981 . 8475) ; ℛ
|
||
;; fraktur
|
||
(120070 . 8493) ; ℭ
|
||
(120075 . 8460) ; ℌ
|
||
(120076 . 8465) ; ℑ
|
||
(120085 . 8476) ; ℜ
|
||
(120092 . 8488) ; ℨ
|
||
;; blackboard
|
||
(120122 . 8450) ; ℂ
|
||
(120127 . 8461) ; ℍ
|
||
(120133 . 8469) ; ℕ
|
||
(120135 . 8473) ; ℙ
|
||
(120136 . 8474) ; ℚ
|
||
(120137 . 8477) ; ℝ
|
||
(120145 . 8484) ; ℤ
|
||
)
|
||
"An alist of deceptive codepoints, and then where the glyph actually resides.")
|
||
|
||
(defun string-offset-apply-roman-char-exceptions (char)
|
||
"Sometimes the codepoint doesn't contain the char you expect.
|
||
Such special cases should be remapped to another value, as given in `string-offset-roman-char-exceptions'."
|
||
(if (assoc char string-offset-roman-char-exceptions)
|
||
(cdr (assoc char string-offset-roman-char-exceptions))
|
||
char))
|
||
|
||
(defun TeX-fold-parenthesize-as-neccesary (tokens &optional suppress-left suppress-right)
|
||
"Add ❪ ❫ parenthesis as if multiple LaTeX tokens appear to be present"
|
||
(if (TeX-string-single-token-p tokens) tokens
|
||
(concat (if suppress-left "" "❪")
|
||
tokens
|
||
(if suppress-right "" "❫"))))
|
||
|
||
(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))
|
||
#+end_src
|
||
|
||
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)
|
||
(setq TeX-electric-math '("\\(" . "")))
|
||
#+end_src
|
||
|
||
Maths deliminators can be de-emphasised a bit
|
||
#+begin_src emacs-lisp
|
||
;; Making \( \) less visible
|
||
(defface unimportant-latex-face
|
||
'((t
|
||
:inherit font-lock-comment-face :family "Overpass" :weight light))
|
||
"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
|
||
*** 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 =;=.
|
||
#+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))))
|
||
#+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))
|
||
#+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
|
||
** R
|
||
*** Editor Visuals
|
||
#+begin_src emacs-lisp
|
||
(after! ess-r-mode
|
||
(appendq! +ligatures-extra-symbols
|
||
'(:assign "⟵"
|
||
:multiply "×"))
|
||
(set-ligatures! 'ess-r-mode
|
||
;; 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
|
||
:assign "<-"
|
||
:multiply "%*%"))
|
||
#+end_src
|
||
** Graphviz
|
||
#+begin_src emacs-lisp
|
||
(use-package! graphviz-dot-mode
|
||
:commands graphviz-dot-mode
|
||
:mode ("\\.dot\\'" "\\.gz\\'")
|
||
:init
|
||
(after! org
|
||
(setcdr (assoc "dot" org-src-lang-modes)
|
||
'graphviz-dot)))
|
||
|
||
(use-package! company-graphviz-dot
|
||
:after graphviz-dot-mode)
|
||
#+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
|
||
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
|
||
** Beancount
|
||
There are a number of rather compelling advantages to [[https://plaintextaccounting.org/][plain text accounting]],
|
||
with [[https://www.ledger-cli.org/][ledger]] being the most obvious example. However, [[https://github.com/beancount/beancount][beancount]], a more recent
|
||
implementation of the idea is ledger-compatable (meaning I can switch easily if
|
||
I change my mind) and has a gorgeous front-end --- [[https://beancount.github.io/fava/][fava]].
|
||
|
||
Of course, there's an Emacs mode for this.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package! beancount
|
||
:mode ("\\.beancount\\'" . beancount-mode)
|
||
:init
|
||
(after! all-the-icons
|
||
(add-to-list 'all-the-icons-icon-alist
|
||
'("\\.beancount\\'" all-the-icons-material "attach_money" :face all-the-icons-lblue))
|
||
(add-to-list 'all-the-icons-mode-icon-alist
|
||
'(beancount-mode all-the-icons-material "attach_money" :face all-the-icons-lblue)))
|
||
:config
|
||
(setq beancount-electric-currency t)
|
||
(defun beancount-bal ()
|
||
"Run bean-report bal."
|
||
(interactive)
|
||
(let ((compilation-read-command nil))
|
||
(beancount--run "bean-report"
|
||
(file-relative-name buffer-file-name) "bal")))
|
||
(map! :map beancount-mode-map
|
||
:n "TAB" #'beancount-align-to-previous-number
|
||
:i "RET" (cmd! (newline-and-indent) (beancount-align-to-previous-number))))
|
||
#+end_src
|