emacs_config/config.org

2849 lines
117 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+title: DOOM Emacs
#+subtitle: Copyright (C) 2025 ─ Vishakh Kumar, Shaurya Singh, Henrik Lissner
#+author: Vishakh Kumar
#+description: A GNU Emacs configuration
#+startup: show2levels indent hidestars
#+options: coverpage:yes
#+property: header-args:emacs-lisp :tangle yes :comments link
#+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
* 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 no
;;; 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 src_elisp{: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
(vertico +icons) ; the search engine of the future
#+end_src
#+name: doom-ui
#+begin_src emacs-lisp
doom-dashboard ; a nifty splash screen for Emacs
doom-quit ; DOOM quit-message prompts when you quit Emacs
(ligatures ; ligatures and symbols to make your code pnoretty again
+extra) ; for those who dislike letters
minimap ; show a map of the code on the side
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
vc-gutter ; vcs diff in the fringe
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
format ; automated prettiness
#+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 ; 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
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
(:if (executable-find "languagetool") grammar) ; tasing grammar mistake every you make
#+end_src
#+name: doom-tools
#+begin_src emacs-lisp
biblio ; Writes a PhD for you (citation needed)
(debugger +lsp) ; FIXME stepping through code, to help you add bugs
(eval +overlay) ; run code, run (also, repls)
(lookup ; helps you navigate your code and documentation
+dictionary ; dictionary/thesaurus is nice
+docsets) ; ...or in Dash docsets locally
lsp ; Language Server Protocol
(magit ; a git porcelain for Emacs
+forge) ; interface with git forges
pdf ; pdf enhancements
rgb ; creating color strings
tree-sitter ; Syntax and Parsing sitting in a tree
#+end_src
#+name: doom-os
#+begin_src emacs-lisp
(:if IS-MAC macos) ; improve compatibility with macOS
#+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...
;;beancount ; mind the GAAP
(cc +lsp +tree-sitter) ; C/C++/Obj-C madness
(clojure +lsp) ; 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
;;dhall ; JSON with FP sprinkles
;;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
;;gdscript ; the language you waited for
;;(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 +lsp) ; the poster child for carpal tunnel syndrome
;;(javascript +lsp) ; all(hope(abandon(ye(who(enter(here))))))
;;(julia +lsp) ; Python, R, and MATLAB in a blender
;;(kotlin +lsp) ; a better, slicker Java(Script)
(latex ; writing papers in Emacs has never been so fun
;;+fold ; fold the clutter away nicities
+latexmk ; modern latex plz
;;+cdlatex ; quick maths symbols
+lsp)
;;lean ; proof that mathematicians need help
;;factor ; for when scripts are stacked against you
;;ledger ; an accounting system in Emacs
(lua +lsp +fennel) ; one-based indices? one-based indices
(markdown +grip) ; writing docs for people to ignore
;;nim ; python + lisp at the speed of c
(nix +tree-sitter) ; 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
+noter ; enhanced PDF notetaking
+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
+roam2) ; wander around notes
;;php ; perl's insecure younger brother
;;plantuml ; diagrams for confusing people more
;;purescript ; javascript, but functional
(python ; beautiful is better than ugly
+lsp
+pyright
+tree-sitter
+conda)
;;qt ; the 'cutest' gui framework ever
;;racket ; a DSL for DSLs
;;raku ; the artist formerly known as perl6
;;(rust
;; +lsp
;; +tree-sitter) ; Fe2O3.unwrap().unwrap().unwrap()
;;scala ; java, but good
;;scheme ; a fully conniving family of lisps
;;(sh +lsp +fish +tree-sitter) ; 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 +lsp) ; the tubes
;;yaml ; JSON, but readable
;;zig ; C, but simpler
#+end_src
***** Everything in Emacs
While interesting, I prefer using Outlook for email, Reeder for RSS, and Jellyfin for media. I'm not too sure if my Emacs config is stable enough to really replace the others, tbh.
#+name: doom-email
#+begin_src emacs-lisp
;; (:if (executable-find "mu") (mu4e +org +gmail))
#+end_src
#+name: doom-app
#+begin_src emacs-lisp
;;calendar ; A dated approach to timetabling
;;emms ; Multimedia in Emacs is music to my ears
;;everywhere ; *leave* Emacs!? You must be joking.
;; (rss +org) ; emacs as an RSS reader
#+end_src
** Packages
:PROPERTIES:
:header-args:emacs-lisp: :tangle no
:END:
Unlike most literate configurations I +am lazy+ like to keep all my packages in
one place
#+name: packages.el
#+attr_html: :collapsed t
#+begin_src emacs-lisp :tangle "packages.el" :noweb no-export :comments no
;; -*- no-byte-compile: t; -*-
;;; $DOOMDIR/packages.el
;;org
<<org>>
;;latex
<<latex>>
;;looks
<<looks>>
;;emacs additions
<<emacs>>
;;fun
<<fun>>
#+end_src
***** Org:
The majority of my work in emacs is done in org mode, even this configuration
was written in org! It makes sense that the majority of my packages are for
tweaking org then
#+name: org
#+begin_src emacs-lisp
(package! doct)
(package! websocket)
(package! org-appear)
(package! org-roam-ui)
(package! org-preview-html)
#+end_src
***** $\LaTeX$:
When I'm not working in org, I'm probably exporting it to latex. Lets adjust
that a bit too
#+name: latex
#+begin_src emacs-lisp
(package! aas)
(package! laas)
(package! engrave-faces)
(package! ox-chameleon
:recipe (:host github :repo "tecosaur/ox-chameleon"))
#+end_src
***** Looks:
Making emacs look good is first priority, actually working in it is second
#+name: looks
#+begin_src emacs-lisp
(package! focus)
(package! dimmer)
(package! minions)
(package! mini-frame)
(package! solaire-mode :disable t)
;; nano stuff
(package! nano-theme)
(package! svg-tag-mode)
;; (package! nano-modeline)
#+end_src
***** Emacs Tweaks:
Emacs is missing just a few packages to improve things here and there. Mainly
- better dictionary support
- improved modal editing
- ebook support
- more colorful docs
#+name: emacs
#+begin_src emacs-lisp
(package! nov)
(package! lexic)
(package! info-colors)
(package! magit-delta :recipe (:host github :repo "dandavison/magit-delta"))
#+end_src
***** Fun:
We do a little trolling (and reading)
#+name: fun
#+begin_src emacs-lisp
(package! xkcd)
(package! md4rd)
(package! smudge)
(package! elcord)
(package! monkeytype)
#+end_src
* Basic Configuration
** Customizations
Customizations done through the emacs gui should go into their own file, in my doom-dir.
#+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
** Personal information
Of course we need to tell emacs who I am
#+begin_src emacs-lisp
(setq user-full-name "Shaurya Singh"
user-mail-address "shaunsingh0207@gmail.com")
#+end_src
** Window management
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 a buffer prompt.
#+begin_src emacs-lisp
(defadvice! prompt-for-buffer (&rest _)
:after '(evil-window-split evil-window-vsplit)
(consult-buffer))
#+end_src
** COMMENT Shell
Vterm is my terminal emulator of choice. We can tell it to use ligatures, and also tell it to compile automatically
Vterm clearly wins the terminal war. Also doesn't need much configuration out of
the box, although the shell integration does.
Fixes a weird bug with native-comp
#+begin_src emacs-lisp
(setq vterm-always-compile-module t)
#+end_src
If the process exits, kill the =vterm= buffer
#+begin_src emacs-lisp
(setq vterm-kill-buffer-on-exit t)
#+end_src
Useful functions for the shell-side integration provided by vterm.
#+begin_src emacs-lisp
(after! vterm
(setf (alist-get "magit-status" vterm-eval-cmds nil nil #'equal)
'((lambda (path)
(magit-status path)))))
#+end_src
Use ligatures from within vterm, we do this by redefining the variable where /not/ to show ligatures. On the other hand, in select modes we want to use extra ligatures, so lets enable that.
#+begin_src emacs-lisp
(setq +ligatures-in-modes t)
#+end_src
** COMMENT LSP
I think the LSP is a bit intrusive (especially with inline suggestions), so lets make it behave a bit more
#+begin_src emacs-lisp
(after! lsp-mode
(setq lsp-enable-symbol-highlighting nil))
(after! lsp-ui
(setq lsp-ui-sideline-enable nil ; no more useful than flycheck
lsp-ui-doc-enable nil)) ; redundant with K
#+end_src
*** Company
I think company is a bit too quick to recommend some stuff
#+begin_src emacs-lisp
(after! company
(setq company-idle-delay 0.1
company-selection-wrap-around t
company-require-match 'never
company-dabbrev-downcase nil
company-dabbrev-ignore-case t
company-dabbrev-other-buffers nil
company-tooltip-limit 5
company-tooltip-minimum-width 40)
(set-company-backend!
'(text-mode
markdown-mode
gfm-mode)
'(:seperate
company-files)))
#+end_src
** Better Defaults
The defaults for emacs aren't so good nowadays. Lets fix that up a bit
#+begin_src emacs-lisp
(setq scroll-margin 2
auto-save-default t
display-line-numbers-type nil
delete-by-moving-to-trash t
truncate-string-ellipsis ""
browse-url-browser-function 'xwidget-webkit-browse-url)
(fringe-mode 0)
(global-subword-mode 1)
#+end_src
There's issues with emacs flickering on mac (and sometimes wayland). This should
fix it
#+begin_src emacs-lisp
(add-to-list 'default-frame-alist '(inhibit-double-buffering . t))
#+end_src
Heres some fixes for yabai, we obviously only want that under darwin (macOS) though
#+begin_src emacs-lisp
(cond
((string-equal system-type "darwin")
(setq frame-resize-pixelwise t
window-resize-pixelwise t)))
#+end_src
** Evil
When we do =s/../..= I usually want a global =/g= at the end, so lets make that the default (along with some other tweaks)
#+begin_src emacs-lisp
(after! evil
(setq evil-ex-substitute-global t ; I like my s/../.. to by global by default
evil-move-cursor-back nil ; Don't move the block cursor when toggling insert mode
evil-kill-on-visual-paste nil)) ; Don't put overwritten text in the kill ring
#+end_src
Which key shows those extra =evil-= hints, feels redundant
#+begin_src emacs-lisp
(setq which-key-allow-multiple-replacements t
which-key-idle-delay 0.5) ;; I need the help, I really do
(after! which-key
(pushnew!
which-key-replacement-alist
'(("" . "\\`+?evil[-:]?\\(?:a-\\)?\\(.*\\)") . (nil . "\\1"))
'(("\\`g s" . "\\`evilem--?motion-\\(.*\\)") . (nil . "\\1"))))
#+end_src
** COMMENT Mu4e
[[xkcd:1796]]
I'm trying out emails in emacs, should be nice. Related, check .mbsyncrc to
setup your emails first. Usually I'll still prefer the mail app, so lets not go all out
#+begin_src emacs-lisp
(after! mu4e
(setq mu4e-index-cleanup nil
mu4e-index-lazy-check t
mu4e-update-interval 300)
(set-email-account! "shaunsingh0207"
'((mu4e-sent-folder . "/Sent Mail")
(mu4e-drafts-folder . "/Drafts")
(mu4e-trash-folder . "/Trash")
(mu4e-refile-folder . "/All Mail")
(smtpmail-smtp-user . "shaunsingh0207@gmail.com"))))
#+end_src
We can also send messages using msmtp
#+begin_src emacs-lisp
(after! mu4e
(setq sendmail-program "msmtp"
send-mail-function #'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from")
message-send-mail-function #'message-send-mail-with-sendmail))
#+end_src
** Magit
Delta is a git diff syntax highlighter written in rust. The author also wrote a package to hook this into the magit diff view (which dont get any syntax highlighting by default). This requires the delta binary. Its packaged on some distributions, but most reliably installed through Rusts package manager cargo.
#+begin_src emacs-lisp
(after! magit
(magit-delta-mode +1))
#+end_src
** COMMENT Monkeytype
Now that we have some nice keyboard sounds, lets test that keyboard with an elisp clone of Monkeytype!
Notably here we want to start in insert mode.
#+begin_src emacs-lisp
(use-package! monkeytype
:commands (monkeytype-region monkeytype-buffer monkeytype-region-as-words)
:config
(setq monkeytype-directory "~/.config/monkeytype"
monkeytype-file-name "%a-%d-%b-%Y-%H-%M-%S"
monkeytype-randomize t
monkeytype-delete-trailing-whitespace t
monkeytype-excluded-chars-regexp "[^[:alnum:]']"))
#+end_src
** COMMENT Smudge
Honestly this probably shouldn't be in emacs, but my music addiction requires it. Those authentication credentials are unique to me, you probably want to change them to your own.
#+begin_src emacs-lisp
(use-package! smudge
:commands global-smudge-remote-mode
:config
(setq smudge-transport 'connect
smudge-oauth2-client-secret "8f5525c076544cd6b25588c868b9b3d7"
smudge-oauth2-client-id "4b2b46899e604b6884714cd7ca47e0e3")
(map! :map smudge-mode-map "M-p" #'smudge-command-map))
#+end_src
* Visual configuration
** COMMENT Dashboard
Lets clean up the dashboard a bit, and add a cute message, whether that be some corporate BS, an developer excuse, or a fun (useless) fact.
#+begin_src emacs-lisp
(setq fancy-splash-image (expand-file-name "misc/splash-images/kaori.png" doom-private-dir) ;; ibm, kaori, fennel
+doom-dashboard-banner-padding '(0 . 0))
(defvar splash-phrase-source-folder
(expand-file-name "misc/splash-phrases" doom-private-dir)
"A folder of text files with a fun phrase on each line.")
(defvar splash-phrase-sources
(let* ((files (directory-files splash-phrase-source-folder nil "\\.txt\\'"))
(sets (delete-dups (mapcar
(lambda (file)
(replace-regexp-in-string "\\(?:-[0-9]+-\\w+\\)?\\.txt" "" file))
files))))
(mapcar (lambda (sset)
(cons sset
(delq nil (mapcar
(lambda (file)
(when (string-match-p (regexp-quote sset) file)
file))
files))))
sets))
"A list of cons giving the phrase set name, and a list of files which contain phrase components.")
(defvar splash-phrase-set
(nth (random (length splash-phrase-sources)) (mapcar #'car splash-phrase-sources))
"The default phrase set. See `splash-phrase-sources'.")
(defun splase-phrase-set-random-set ()
"Set a new random splash phrase set."
(interactive)
(setq splash-phrase-set
(nth (random (1- (length splash-phrase-sources)))
(cl-set-difference (mapcar #'car splash-phrase-sources) (list splash-phrase-set))))
(+doom-dashboard-reload t))
(defvar splase-phrase--cache nil)
(defun splash-phrase-get-from-file (file)
"Fetch a random line from FILE."
(let ((lines (or (cdr (assoc file splase-phrase--cache))
(cdar (push (cons file
(with-temp-buffer
(insert-file-contents (expand-file-name file splash-phrase-source-folder))
(split-string (string-trim (buffer-string)) "\n")))
splase-phrase--cache)))))
(nth (random (length lines)) lines)))
(defun splash-phrase (&optional set)
"Construct a splash phrase from SET. See `splash-phrase-sources'."
(mapconcat
#'splash-phrase-get-from-file
(cdr (assoc (or set splash-phrase-set) splash-phrase-sources))
" "))
(defun doom-dashboard-phrase ()
"Get a splash phrase, flow it over multiple lines as needed, and make fontify it."
(mapconcat
(lambda (line)
(+doom-dashboard--center
+doom-dashboard--width
(with-temp-buffer
(insert-text-button
line
'action
(lambda (_) (+doom-dashboard-reload t))
'face 'doom-dashboard-menu-title
'mouse-face 'doom-dashboard-menu-title
'help-echo "Random phrase"
'follow-link t)
(buffer-string))))
(split-string
(with-temp-buffer
(insert (splash-phrase))
(setq fill-column (min 70 (/ (* 2 (window-width)) 3)))
(fill-region (point-min) (point-max))
(buffer-string))
"\n")
"\n"))
(defadvice! doom-dashboard-widget-loaded-with-phrase ()
:override #'doom-dashboard-widget-loaded
(setq line-spacing 0.2)
(insert
"\n\n"
(propertize
(+doom-dashboard--center
+doom-dashboard--width
(doom-display-benchmark-h 'return))
'face 'doom-dashboard-loaded)
"\n"
(doom-dashboard-phrase)
"\n"))
;; remove useless dashboard info
(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-shortmenu)
(add-hook! '+doom-dashboard-mode-hook (hide-mode-line-mode 1) (hl-line-mode -1))
(setq-hook! '+doom-dashboard-mode-hook evil-normal-state-cursor (list nil))
#+end_src
** Info Colors
This makes manual pages nicer to look at by adding variable pitch fontification
and colouring.
To use this we'll just hook it into =Info=.
#+begin_src emacs-lisp
(use-package! info-colors
:commands (info-colors-fontify-node))
(add-hook 'Info-selection-hook 'info-colors-fontify-node)
#+end_src
** COMMENT Minibuffer
Here we set up a cute minibuffer to better fit with our nano-themed niceties. A little more intriqute (and slower) than the default doom setups, but its nice to have.
#+begin_src emacs-lisp
(setq minibuffer-prompt-properties '(read-only t
cursor-intangible t
face minibuffer-prompt)
enable-recursive-minibuffers t)
(defun my/minibuffer-header ()
"Minibuffer header"
(let ((depth (minibuffer-depth)))
(concat
(propertize (concat "" (if (> depth 1)
(format "Minibuffer (%d)" depth)
"Minibuffer ")
"\n")
'face `(:inherit (nano-subtle nano-strong)
:box (:line-width (1 . 3)
:color ,(face-background 'nano-subtle)
:style flat)
:extend t)))))
(defun my/mini-frame-reset (frame)
"Reset FRAME size and position.
Move frame at the top of parent frame and resize it
horizontally to fit the width of current selected window."
(interactive)
(let* ((border (frame-parameter frame 'internal-border-width))
(height (frame-parameter frame 'height)))
(with-selected-frame (frame-parent frame)
(let* ((edges (window-pixel-edges))
(body-edges (window-body-pixel-edges))
(top (nth 1 edges))
(bottom (nth 3 body-edges))
(left (- (nth 0 edges) (or left-fringe-width 0)))
(right (+ (nth 2 edges) (or right-fringe-width 0)))
(width (- right left))
(y (- top border)))
(set-frame-width frame width nil t)
(set-frame-height frame height)
(set-frame-position frame (- left border) y)))))
(defun my/mini-frame-shrink (frame &optional delta)
"Make the FRAME DELTA lines smaller.
If no argument is given, make the frame one line smaller. If
DELTA is negative, enlarge frame by -DELTA lines."
(interactive)
(let ((delta (or delta -1)))
(when (and (framep frame)
(frame-live-p frame)
(frame-visible-p frame))
(set-frame-parameter frame 'height
(+ (frame-parameter frame 'height) delta)))))
(defun my/minibuffer-setup ()
"Install a header line in the minibuffer via an overlay (and a hook)"
(set-window-margins nil 0 0)
(set-fringe-style '(0 . 0))
(cursor-intangible-mode t)
(face-remap-add-relative 'default
:inherit 'highlight)
(let* ((overlay (make-overlay (+ (point-min) 0) (+ (point-min) 0)))
(inhibit-read-only t))
(save-excursion
(goto-char (point-min))
(insert (propertize
(concat (my/minibuffer-header)
(propertize "\n" 'face `(:height 0.33))
(propertize " "))
'cursor-intangible t
'read-only t
'field t
'rear-nonsticky t
'front-sticky t)))))
(add-hook 'minibuffer-setup-hook #'my/minibuffer-setup)
#+end_src
** Mini-Frame
And to go with that, we want to put our minibuffer in a posframe. This can either be placed at the bottom or top of the window, align it with your statusline.
#+begin_src emacs-lisp
(use-package! mini-frame
:hook (after-init . mini-frame-mode)
:config
(defcustom my/minibuffer-position 'top
"Minibuffer position, one of 'top or 'bottom"
:type '(choice (const :tag "Top" top)
(const :tag "Bottom" bottom))
:group 'nano-minibuffer)
(defun my/minibuffer--frame-parameters ()
"Compute minibuffer frame size and position."
;; Quite precise computation to align the minibuffer and the
;; modeline when they are both at top position
(let* ((edges (window-pixel-edges)) ;; (left top right bottom)
(body-edges (window-body-pixel-edges)) ;; (left top right bottom)
(left (nth 0 edges)) ;; Take margins into account
(top (nth 1 edges)) ;; Drop header line
(right (nth 2 edges)) ;; Take margins into account
(bottom (nth 3 body-edges)) ;; Drop header line
(left (if (eq left-fringe-width 0)
left
(- left (frame-parameter nil 'left-fringe))))
(right (nth 2 edges))
(right (if (eq right-fringe-width 0)
right
(+ right (frame-parameter nil 'right-fringe))))
(border 1)
(width (- right left (* 0 border)))
;; Window divider mode
(width (- width (if (and (bound-and-true-p window-divider-mode)
(or (eq window-divider-default-places 'right-only)
(eq window-divider-default-places t))
(window-in-direction 'right (selected-window)))
window-divider-default-right-width
0)))
(y (- top border)))
(append `((left-fringe . 0)
(right-fringe . 0)
(user-position . t)
(foreground-color . ,(face-foreground 'highlight nil 'default))
(background-color . ,(face-background 'highlight nil 'default)))
(cond ((and (eq my/minibuffer-position 'bottom))
`((top . -1)
(left . 0)
(width . 1.0)
(child-frame-border-width . 0)
(internal-border-width . 0)))
(t
`((left . ,(- left border))
(top . ,y)
(width . (text-pixels . ,width))
(child-frame-border-width . ,border)
(internal-border-width . ,border)))))))
(set-face-background 'child-frame-border (face-foreground 'nano-faded))
(setq mini-frame-default-height 3)
(setq mini-frame-create-lazy t)
(setq mini-frame-show-parameters 'my/minibuffer--frame-parameters)
(setq mini-frame-ignore-commands
'("edebug-eval-expression" debugger-eval-expression))
(setq mini-frame-internal-border-color (face-foreground 'nano-faded))
(setq mini-frame-resize-min-height 3)
(setq mini-frame-resize t)
(defun my/mini-frame (&optional height foreground background border)
"Create a child frame positionned over the header line whose
width corresponds to the width of the current selected window.
The HEIGHT in lines can be specified, as well as the BACKGROUND
color of the frame. BORDER width (pixels) and color (FOREGROUND)
can be also specified."
(interactive)
(let* ((foreground (or foreground
(face-foreground 'font-lock-comment-face nil t)))
(background (or background (face-background 'highlight nil t)))
(border (or border 1))
(height (round (* (or height 8) (window-font-height))))
(edges (window-pixel-edges))
(body-edges (window-body-pixel-edges))
(top (nth 1 edges))
(bottom (nth 3 body-edges))
(left (- (nth 0 edges) (or left-fringe-width 0)))
(right (+ (nth 2 edges) (or right-fringe-width 0)))
(width (- right left))
;; Window divider mode
(width (- width (if (and (bound-and-true-p window-divider-mode)
(or (eq window-divider-default-places 'right-only)
(eq window-divider-default-places t))
(window-in-direction 'right (selected-window)))
window-divider-default-right-width
0)))
(y (- top border))
(child-frame-border (face-attribute 'child-frame-border :background)))
(set-face-attribute 'child-frame-border t :background foreground)
(let ((frame (make-frame
`((parent-frame . ,(window-frame))
(delete-before . ,(window-frame))
(minibuffer . nil)
(modeline . nil)
(left . ,(- left border))
(top . ,y)
(width . (text-pixels . ,width))
(height . (text-pixels . ,height))
;; (height . ,height)
(child-frame-border-width . ,border)
(internal-border-width . ,border)
(background-color . ,background)
(horizontal-scroll-bars . nil)
(menu-bar-lines . 0)
(tool-bar-lines . 0)
(desktop-dont-save . t)
(unsplittable . nil)
(no-other-frame . t)
(undecorated . t)
(pixelwise . t)
(visibility . t)))))
(set-face-attribute 'child-frame-border t :background child-frame-border)
frame))))
#+end_src
** Minad Suite
I feel in love with these packages right away, so much better than icky ivy!
*** Vertico
Small tweaks, just some themeing here and there to better fit with our minibuffer changes
#+begin_src emacs-lisp
(after! vertico
;; settings
(setq vertico-resize nil ; How to resize the Vertico minibuffer window.
vertico-count 10 ; Maximal number of candidates to show.
vertico-count-format nil) ; No prefix with number of entries
;; looks
(setq vertico-grid-separator
#(" | " 2 3 (display (space :width (1))
face (:background "#ECEFF1")))
vertico-group-format
(concat #(" " 0 1 (face vertico-group-title))
#(" " 0 1 (face vertico-group-separator))
#(" %s " 0 4 (face vertico-group-title))
#(" " 0 1 (face vertico-group-separator
display (space :align-to (- right (-1 . right-margin) (- +1)))))))
(set-face-attribute 'vertico-group-separator nil
:strike-through t)
(set-face-attribute 'vertico-current nil
:inherit '(nano-strong nano-subtle))
(set-face-attribute 'completions-first-difference nil
:inherit '(nano-default))
;; minibuffer tweaks
(defun my/vertico--resize-window (height)
"Resize active minibuffer window to HEIGHT."
(setq-local truncate-lines t
resize-mini-windows 'grow-only
max-mini-window-height 1.0)
(unless (frame-root-window-p (active-minibuffer-window))
(unless vertico-resize
(setq height (max height vertico-count)))
(let* ((window-resize-pixelwise t)
(dp (- (max (cdr (window-text-pixel-size))
(* (default-line-height) (1+ height)))
(window-pixel-height))))
(when (or (and (> dp 0) (/= height 0))
(and (< dp 0) (eq vertico-resize t)))
(window-resize nil dp nil nil 'pixelwise)))))
(advice-add #'vertico--resize-window :override #'my/vertico--resize-window)
;; completion at point
(setq completion-in-region-function
(lambda (&rest args)
(apply (if vertico-mode
#'consult-completion-in-region
#'completion--in-region)
args)))
(defun minibuffer-format-candidate (orig cand prefix suffix index _start)
(let ((prefix (if (= vertico--index index)
""
" ")))
(funcall orig cand prefix suffix index _start)))
(advice-add #'vertico--format-candidate
:around #'minibuffer-format-candidate)
(defun vertico--prompt-selection ()
"Highlight the prompt"
(let ((inhibit-modification-hooks t))
(set-text-properties (minibuffer-prompt-end) (point-max)
'(face (nano-strong nano-salient)))))
(defun minibuffer-vertico-setup ()
(setq truncate-lines t)
(setq completion-in-region-function
(if vertico-mode
#'consult-completion-in-region
#'completion--in-region)))
(add-hook 'vertico-mode-hook #'minibuffer-vertico-setup)
(add-hook 'minibuffer-setup-hook #'minibuffer-vertico-setup))
#+end_src
*** Marginalia
More small tweaks
#+begin_src emacs-lisp
(after! marginalia
(setq marginalia--ellipsis "" ; Nicer ellipsis
marginalia-align 'right ; right alignment
marginalia-align-offset -1)) ; one space on the right
#+end_src
** COMMENT Elcord
Whats even the point of using Emacs unless youre constantly telling everyone about it? What we're doing here is replacing the buffer details with something less revealing, then replacing the icon set used via creating a custom discord application. Theoretically this config should work for anyone, but I haven't tested it yet. Thank you cae for the [[file:./misc/lang_icons][icons]]
#+begin_src emacs-lisp
(defun shaunsingh/elcord-buffer-details-format ()
"Return the buffer details string shown on discord."
(format "Text is a Magical Thing"))
(use-package! elcord
:commands elcord-mode
:config
(setq elcord-mode-icon-alist '((dashboard-mode . "elisp-mode_icon")
(fundamental-mode . "elisp-mode_icon")
(c-mode . "c-mode_icon")
(c++-mode . "c_-mode_icon")
(crystal-mode . "crystal-mode_icon")
(clojure-mode . "clojure-mode_icon")
(css-mode . "css-mode_icon")
(emacs-lisp-mode . "elisp-mode_icon")
(eshell-mode . "elisp-mode_icon")
(haskell-mode . "haskell-mode_icon")
(haxe-mode . "haxe-mode_icon")
(haskell-interactive-mode . "haskell-mode_icon")
(js-mode . "javascript-mode_icon")
(magit-mode . "magit-mode_icon")
(markdown-mode . "markdown-mode_icon")
(nixos-mode . "nixos-mode_icon")
(latex-mode . "latex-mode_icon")
(text-mode . "elisp-mode_icon")
(org-mode . "org-mode_icon")
("^slime-.*" . "lisp-mode_icon")
("^sly-.*$" . "lisp-mode_icon")
(typescript-mode . "typescript-mode_icon")
(writer-mode . "org-mode_icon")
(term-mode . "x-mode_icon")
(shell-mode . "x-mode_icon")
(vterm-mode . "x-mode_icon")))
(setq elcord-client-id "930927487867834408") ;; You can set your own check elcord's readme
(setq elcord-quiet t
elcord-editor-icon "elisp-mode_icon"
elcord-buffer-details-format-function 'shaunsingh/elcord-buffer-details-format
elcord-display-buffer-details t
elcord-display-elapsed nil
elcord-show-small-icon nil
elcord-use-major-mode-as-main-icon t
elcord-refresh-rate 0.25))
#+end_src
** Pixel-scroll
Default doom scrolling is pretty slow, so lets improve on that with pixel-scrolling. However, =emacs-mac= has its own version of pixel scroll, and so does =emacs29=, so we want to enable this under specific cases
#+begin_src emacs-lisp
(if (boundp 'mac-mouse-wheel-smooth-scroll)
(setq mac-mouse-wheel-smooth-scroll t))
(if (> emacs-major-version 28)
(pixel-scroll-precision-mode))
#+end_src
** Nano
Some UI tweaks to make emacs comfier
Lets start off by just giving the text a little more space to breathe
#+begin_src emacs-lisp
(setq-default line-spacing 0.24)
#+end_src
*** Window Padding
Making things spacier. Add padding around emacs and between splits
#+begin_src emacs-lisp
;; Vertical window divider
(setq-default window-divider-default-right-width 24
window-divider-default-places 'right-only
left-margin-width 0
right-margin-width 0
window-combination-resize nil) ; Do not resize windows proportionally
(window-divider-mode 1)
#+end_src
#+begin_src emacs-lisp
;; Default frame settings
(setq default-frame-alist '((min-height . 1) '(height . 45)
(min-width . 1) '(width . 81)
(vertical-scroll-bars . nil)
(internal-border-width . 24)
(left-fringe . 0)
(right-fringe . 0)
(tool-bar-lines . 0)
(menu-bar-lines . 0)))
(setq initial-frame-alist default-frame-alist)
#+end_src
*** Colorscheme
We want to use the nano theme created by the excellent rougier. Heres a small function to change the appearence of the theme based on the system setting. I find myself preferring the light theme, so its disabled but here it is anyways.
#+begin_src emacs-lisp
(defun shaunsingh/apply-nano-theme (appearance)
"Load theme, taking current system APPEARANCE into consideration."
(mapc #'disable-theme custom-enabled-themes)
(pcase appearance
('light (nano-light))
('dark (nano-dark))))
#+end_src
And now to setup the actual theme. Some extra faces I added because doom modules lookd odd without them
#+begin_src emacs-lisp
(use-package nano-theme
:hook (after-init . nano-light)
:config
;; If emacs has been built with system appearance detection
;; add a hook to change the theme to match the system
;; (if (boundp 'ns-system-appearance-change-functions)
;; (add-hook 'ns-system-appearance-change-functions #'shaunsingh/apply-nano-theme))
;; Now to add some missing faces
(custom-set-faces
`(flyspell-incorrect ((t (:underline (:color ,nano-light-salient :style line)))))
`(flyspell-duplicate ((t (:underline (:color ,nano-light-salient :style line)))))
`(git-gutter:modified ((t (:foreground ,nano-light-salient))))
`(git-gutter-fr:added ((t (:foreground ,nano-light-popout))))
`(git-gutter-fr:modified ((t (:foreground ,nano-light-salient))))
`(lsp-ui-doc-url:added ((t (:background ,nano-light-highlight))))
`(lsp-ui-doc-background:modified ((t (:background ,nano-light-highlight))))
`(vterm-color-red ((t (:foreground ,nano-light-critical))))
`(vterm-color-blue ((t (:foreground ,nano-light-salient))))
`(vterm-color-green ((t (:foreground ,nano-light-popout))))
`(vterm-color-yellow ((t (:foreground ,nano-light-popout))))
`(vterm-color-magenta ((t (:foreground ,nano-light-salient))))
`(scroll-bar ((t (:background ,nano-light-background))))
`(child-frame-border ((t (:foreground ,nano-light-faded))))
`(avy-lead-face-1 ((t (:foreground ,nano-light-subtle))))
`(avy-lead-face ((t (:foreground ,nano-light-popout :weight bold))))
`(avy-lead-face-0 ((t (:foreground ,nano-light-salient :weight bold))))))
#+end_src
Originally I was going to use nano-modeline, but I prefer the default one anyways. We can use the excellent minions package to clean it up though.
#+begin_src emacs-lisp
;; (use-package! nano-modeline
;; :hook (after-init . nano-modeline-mode)
;; :config
;; (setq nano-modeline-prefix 'status
;; nano-modeline-prefix-padding 1
;; nano-modeline-position 'bottom))
(use-package! minions
:hook (after-init . minions-mode))
;; Add a zero-width tall character to add padding to modeline
(setq-default mode-line-format
(cons (propertize "\u200b" 'display '((raise -0.35) (height 1.4))) mode-line-format))
#+end_src
** COMMENT SVG-tag-mode
Replaced org-modern with this, a little heavier on emacs but looks better (imo).
#+begin_src emacs-lisp
(use-package svg-tag-mode
:commands svg-tag-mode
:config
(defconst date-re "[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}")
(defconst time-re "[0-9]\\{2\\}:[0-9]\\{2\\}")
(defconst day-re "[A-Za-z]\\{3\\}")
(defconst day-time-re (format "\\(%s\\)? ?\\(%s\\)?" day-re time-re))
(defun svg-progress-percent (value)
(svg-image (svg-lib-concat
(svg-lib-progress-bar (/ (string-to-number value) 100.0)
nil :margin 0 :stroke 2 :radius 3 :padding 2 :width 11)
(svg-lib-tag (concat value "%")
nil :stroke 0 :margin 0)) :ascent 'center))
(defun svg-progress-count (value)
(let* ((seq (mapcar #'string-to-number (split-string value "/")))
(count (float (car seq)))
(total (float (cadr seq))))
(svg-image (svg-lib-concat
(svg-lib-progress-bar (/ count total) nil
:margin 0 :stroke 2 :radius 3 :padding 2 :width 11)
(svg-lib-tag value nil
:stroke 0 :margin 0)) :ascent 'center)))
(setq svg-tag-tags
`(
;; Org tags
(":\\([A-Za-z0-9]+\\)" . ((lambda (tag) (svg-tag-make tag))))
(":\\([A-Za-z0-9]+[ \-]\\)" . ((lambda (tag) tag)))
;; Task priority
("\\[#[A-Z]\\]" . ( (lambda (tag)
(svg-tag-make tag :face 'org-priority
:beg 2 :end -1 :margin 0))))
;; Progress
("\\(\\[[0-9]\\{1,3\\}%\\]\\)" . ((lambda (tag)
(svg-progress-percent (substring tag 1 -2)))))
("\\(\\[[0-9]+/[0-9]+\\]\\)" . ((lambda (tag)
(svg-progress-count (substring tag 1 -1)))))
;; TODO / DONE
("TODO" . ((lambda (tag) (svg-tag-make "TODO" :face 'org-todo :inverse t :margin 0))))
("DONE" . ((lambda (tag) (svg-tag-make "DONE" :face 'org-done :margin 0))))
;; Citation of the form [cite:@Knuth:1984]
("\\(\\[cite:@[A-Za-z]+:\\)" . ((lambda (tag)
(svg-tag-make tag
:inverse t
:beg 7 :end -1
:crop-right t))))
("\\[cite:@[A-Za-z]+:\\([0-9]+\\]\\)" . ((lambda (tag)
(svg-tag-make tag
:end -1
:crop-left t))))
;; Active date (with or without day name, with or without time)
(,(format "\\(<%s>\\)" date-re) .
((lambda (tag)
(svg-tag-make tag :beg 1 :end -1 :margin 0))))
(,(format "\\(<%s \\)%s>" date-re day-time-re) .
((lambda (tag)
(svg-tag-make tag :beg 1 :inverse nil :crop-right t :margin 0))))
(,(format "<%s \\(%s>\\)" date-re day-time-re) .
((lambda (tag)
(svg-tag-make tag :end -1 :inverse t :crop-left t :margin 0))))
;; Inactive date (with or without day name, with or without time)
(,(format "\\(\\[%s\\]\\)" date-re) .
((lambda (tag)
(svg-tag-make tag :beg 1 :end -1 :margin 0 :face 'org-date))))
(,(format "\\(\\[%s \\)%s\\]" date-re day-time-re) .
((lambda (tag)
(svg-tag-make tag :beg 1 :inverse nil :crop-right t :margin 0 :face 'org-date))))
(,(format "\\[%s \\(%s\\]\\)" date-re day-time-re) .
((lambda (tag)
(svg-tag-make tag :end -1 :inverse t :crop-left t :margin 0 :face 'org-date)))))))
#+end_src
** Dimming
#+begin_src emacs-lisp
;; Dim inactive windows
(use-package! dimmer
:hook (after-init . dimmer-mode)
:config
(setq dimmer-fraction 0.5
dimmer-adjustment-mode :foreground
dimmer-use-colorspace :rgb
dimmer-watch-frame-focus-events nil)
(dimmer-configure-which-key)
(dimmer-configure-magit)
(dimmer-configure-posframe))
#+end_src
Similar to that, I want to dim surrounding text using the focus package
#+begin_src emacs-lisp
(defun add-list-to-list (dst src)
"Similar to `add-to-list', but accepts a list as 2nd argument"
(set dst
(append (eval dst) src)))
(use-package! focus
:commands focus-mode
:config
;; add whatever lsp servers you use to this list
(add-list-to-list 'focus-mode-to-thing
'((lua-mode . lsp-folding-range)
(rust-mode . lsp-folding-range)
(latex-mode . lsp-folding-range)
(python-mode . lsp-folding-range))))
#+end_src
** 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
** COMMENT RSS
RSS is a nice simple way of getting my news. Lets set that up
#+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)
(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))
(after! elfeed
(elfeed-org)
(use-package! elfeed-link)
(setq rmh-elfeed-org-files '("~/org/elfeed.org"))
(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
(setq-local truncate-lines nil
shr-width 120
visual-fill-column-center-text t
default-text-properties '(line-height 1.1))
(let ((inhibit-read-only t)
(inhibit-modification-hooks 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))))
(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)))))
(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
** COMMENT Ebooks
[[xkcd:548]]
To actually read the ebooks we use =nov=.
#+begin_src emacs-lisp
(use-package! nov
:mode ("\\.epub\\'" . nov-mode)
:config
(map! :map nov-mode-map
:n "RET" #'nov-scroll-up)
(advice-add 'nov-render-title :override #'ignore)
(defun +nov-mode-setup ()
(face-remap-add-relative 'default :height 1.3)
(setq-local 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 81
nov-text-width 80)
(visual-fill-column-mode 1)
(add-to-list '+lookup-definition-functions #'+lookup/dictionary-definition)
(add-hook 'nov-mode-hook #'+nov-mode-setup)))
#+end_src
* Org
** Org-Mode
I really like org mode, I've given some thought to why, and below is the result.
#+attr_latex: :align *{8}{p{0.105\linewidth}} :font \small
| Format | Fine-grained control | Initial ease of use | 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 |
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.
I prefer /org as my directory. Lets change some other defaults too
#+begin_src emacs-lisp
(after! org
(setq org-directory "~/org" ; let's put files here
org-ellipsis "" ; cute icon for folded org blocks
org-list-allow-alphabetical t ; have a. A. a) A) list bullets
org-use-property-inheritance t ; it's convenient to have properties inherited
org-catch-invisible-edits 'smart ; try not to accidently do weird stuff in invisible regions
org-log-done 'time ; having the time a item is done sounds convenient
org-roam-directory "~/org/roam/")) ; same thing, for roam
#+end_src
And some extra fontification doesn't hurt
#+begin_src emacs-lisp
(after! org
(setq org-src-fontify-natively t
org-fontify-whole-heading-line t
org-inline-src-prettify-results '("" . "")
org-fontify-done-headline t
org-fontify-quote-and-verse-blocks t))
#+end_src
I want to slightly change the default args for babel
#+begin_src emacs-lisp
(after! org
(setq org-babel-default-header-args
'((:session . "none")
(:results . "replace")
(:exports . "code")
(:cache . "no")
(:noweb . "no")
(:hlines . "no")
(:tangle . "no")
(:comments . "link"))))
#+end_src
I also want to change the order of bullets
#+begin_src emacs-lisp
(after! org
(setq org-list-demote-modify-bullet '(("+" . "-") ("-" . "+") ("*" . "+") ("1." . "a."))))
#+end_src
And the default dashes and =+= signs just don't cut it anymore. Lets make them fancy bullets instead
#+begin_src emacs-lisp
(font-lock-add-keywords 'org-mode
'(("^ *\\([-]\\) "
(0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ""))))))
(font-lock-add-keywords 'org-mode
'(("^ *\\([+]\\) "
(0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ""))))))
#+end_src
The =[[yt:...]]= links preview nicely, but dont export nicely. Thankfully, we can fix that.
#+begin_src emacs-lisp
(after! ox
(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
*** HTML export
Inspired by Tecosaur's amazing org-css, I wanted to make my own, but with fewer features and slightly cleaner overall.
#+begin_src emacs-lisp
(defun org-inline-css-hook (exporter)
"Insert custom inline css"
(when (eq exporter 'html)
(let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
(path (concat dir "style.css"))
(homestyle (or (null dir) (null (file-exists-p path))))
(final (if homestyle (expand-file-name "misc/org-css/style.css" doom-private-dir) path)))
(setq org-html-head-include-default-style nil)
(setq org-html-head (concat
"<style type=\"text/css\">\n"
"<!--/*--><![CDATA[/*><!--*/\n"
(with-temp-buffer
(insert-file-contents final)
(buffer-string))
"/*]]>*/-->\n"
"</style>\n")))))
(defun org-inline-js-hook (exporter)
"Insert custom inline css"
(when (eq exporter 'html)
(let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
(path (concat dir "style.js"))
(homestyle (or (null dir) (null (file-exists-p path))))
(final (if homestyle (expand-file-name "misc/org-css/style.js" doom-private-dir) path)))
(setq org-html-head-include-default-style nil)
(setq org-html-head (concat
"<script type=\"text/javascript\">\n"
"<!--/*--><![CDATA[/*><!--*/\n"
(with-temp-buffer
(insert-file-contents final)
(buffer-string))
"/*]]>*/-->\n"
"</script>\n")))))
(defun org-inline-html-hook (exporter)
"Insert custom inline css"
(when (eq exporter 'html)
(let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
(path (concat dir "style.html"))
(homestyle (or (null dir) (null (file-exists-p path))))
(final (if homestyle (expand-file-name "misc/org-css/style.html" doom-private-dir) path)))
(setq org-html-head-include-default-style nil)
(setq org-html-head (concat
(with-temp-buffer
(insert-file-contents final)
(buffer-string))
"\n")))))
(add-hook 'org-export-before-processing-hook 'org-inline-css-hook)
(add-hook 'org-export-before-processing-hook 'org-inline-js-hook)
(add-hook 'org-export-before-processing-hook 'org-inline-html-hook)
#+end_src
If MathJax is used, we want to use version 3 instead of the default version 2.
Looking at a [[https://www.intmath.com/cg5/katex-mathjax-comparison.php][comparison]] we seem to find that it is ~5 times as fast, uses a
single file instead of multiple, but seems to be a bit bigger unfortunately.
Thankfully this can be mitigated my adding the ~async~ attribute to defer loading.
#+begin_src emacs-lisp
(after! ox-html
(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
And now to preview that export live
#+begin_src emacs-lisp
(use-package! org-preview-html
:commands org-preview-html-mode
:config
(setq org-preview-html-refresh-configuration 'save
org-preview-html-viewer 'xwidget))
#+end_src
I like to preview images inline too
#+begin_src emacs-lisp
(setq org-startup-with-inline-images t)
#+end_src
** COMMENT Org-Roam
Lets set up =org-roam-ui=
#+begin_src emacs-lisp
(use-package! websocket
:after org-roam)
(use-package! org-roam-ui
:after org-roam
:commands org-roam-ui-open
:config
(setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t
org-roam-ui-open-on-start t))
#+end_src
Now, I want to replace the org-roam buffer with org-roam-ui, to do that, we need
to disable the regular buffer
#+begin_src emacs-lisp
(after! org-roam
(setq +org-roam-open-buffer-on-find-file nil))
#+end_src
** COMMENT Org-Agenda
Set the directory
#+begin_src emacs-lisp
(after! org-agenda
(setq org-agenda-files (list "~/org/school.org"
"~/org/todo.org"))
(setq org-agenda-window-setup 'current-window
org-agenda-restore-windows-after-quit t
org-agenda-show-all-dates nil
org-agenda-time-in-grid t
org-agenda-show-current-time-in-grid t
org-agenda-start-on-weekday 1
org-agenda-span 7
org-agenda-tags-column 0
org-agenda-block-separator nil
org-agenda-category-icon-alist nil
org-agenda-sticky t)
(setq org-agenda-prefix-format
'((agenda . "%i %?-12t%s")
(todo . "%i")
(tags . "%i")
(search . "%i")))
(setq org-agenda-sorting-strategy
'((agenda deadline-down scheduled-down todo-state-up time-up
habit-down priority-down category-keep)
(todo priority-down category-keep)
(tags timestamp-up priority-down category-keep)
(search category-keep))))
#+end_src
** COMMENT Font Display
It seems reasonable to have deadlines in the error face when they're passed.
#+begin_src emacs-lisp
(after! org
(setq org-agenda-deadline-faces
'((1.0 . error)
(1.0 . org-warning)
(0.5 . org-upcoming-deadline)
(0.0 . org-upcoming-distant-deadline))))
#+end_src
And lets conceal *those* /syntax/ +markers+.
#+begin_src emacs-lisp
(use-package! org-appear
:after org
:hook (org-mode . org-appear-mode)
:config
(setq org-appear-autoemphasis t
org-appear-autolinks t
org-appear-autosubmarkers t))
#+end_src
*** (sub|super)script characters
Annoying having to gate these, so let's fix that
#+begin_src emacs-lisp
(setq org-export-with-sub-superscripts '{})
#+end_src
*** Make verbatim different to code
Since have just gone to so much effort above let's make the most of it by making
=verbatim= use ~verb~ instead of ~protectedtexttt~ (default).
#+begin_src emacs-lisp
(after! org
(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
** COMMENT XKCD
[[xkcd:446]]
#+begin_quote
Relevent XKCD:
#+end_quote
I link to xkcd's so much that its better to just have a configuration for them
We want to set this up so it loads nicely in org.
#+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))
:general (:states 'normal
:keymaps 'xkcd-mode-map
"<right>" #'xkcd-next
"n" #'xkcd-next
"<left>" #'xkcd-prev
"N" #'xkcd-prev
"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 `completing-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 (completing-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 completing-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
Now to just have this register with org
#+begin_src emacs-lisp
(after! org
(org-link-set-parameters "xkcd"
:image-data-fun #'+org-xkcd-image-fn
:follow #'+org-xkcd-open-fn
:export #'+org-xkcd-export
:complete #'+org-xkcd-complete)
(defun +org-xkcd-open-fn (link)
(+org-xkcd-image-fn nil link nil))
(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
** COMMENT 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
*** Dictionaries
On every system I use (I have a lot of systems) the dictionary results in doom/emacs are different, and that gets annoying. Lets use lexic + stardict, instead of the default dictionaries.
#+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))))
(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
* COMMENT LaTeX
Note that this \(\LaTeX\) export configuration is largely borrowed from Tecosaur and Elken, and just adjusted to fit my needs. Big thanks to them for their excellent configs.
** PDF-Tools
DocView gives me a headache, but pdf-tools can be improved, lets configure it a little more
#+begin_src emacs-lisp
(use-package! pdf-view
:hook (pdf-tools-enabled . pdf-view-themed-minor-mode)
:config
(setq pdf-view-use-scaling t
pdf-view-use-imagemagick nil
pdf-view-display-size 'fit-page))
#+end_src
*** View Exported File
I have to export files pretty often, lets setup some keybindings to make it easier
#+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-bufferpop-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
** \(\LaTeX\) highlighting in Org-mode
The default inline latex highlighting is a bit bland. Normally this would be fixed with the =+pretty= flag, but that has its own issues. Lets just stick to enabling it manually.
#+begin_src emacs-lisp
(after! org
(setq org-highlight-latex-and-related '(native script entities))
(add-to-list 'org-src-block-faces '("latex" (:inherit default :extend t))))
#+end_src
Our org-mode config gets a bit expensive though. Lets make a small mode with just the basics, for exporting
#+begin_src emacs-lisp
(defun +org-mode--fontlock-only-mode ()
"Just apply org-mode's font-lock once."
(let (org-mode-hook
org-hide-leading-stars
org-hide-emphasis-markers)
(org-set-font-lock-defaults)
(font-lock-ensure))
(setq-local major-mode #'fundamental-mode))
(defun +org-export-babel-mask-org-config (_backend)
"Use `+org-mode--fontlock-only-mode' instead of `org-mode'."
(setq-local org-src-lang-modes
(append org-src-lang-modes
(list (cons "org" #'+org-mode--fontlock-only)))))
(add-hook 'org-export-before-processing-hook #'+org-export-babel-mask-org-config)
#+end_src
** Tectonic
Tectonic is the hot new thing, which also means I can get rid of my tex installation.
#+begin_src emacs-lisp
(after! org
(setq-default org-latex-pdf-process '("tectonic -Z shell-escape --outdir=%o %f")))
#+end_src
Looks crisp!
\begin{align*}
f(x) &= x^2\\
g(x) &= \frac{1}{x}\\
F(x) &= \int^a_b \frac{1}{3}x^3
\end{align*}
** Preambles
Various preamble setups to improve the overall look of several items
#+begin_src emacs-lisp
(defvar org-latex-caption-preamble "
\\usepackage{subcaption}
\\usepackage[hypcap=true]{caption}
\\setkomafont{caption}{\\sffamily\\small}
\\setkomafont{captionlabel}{\\upshape\\bfseries}
\\captionsetup{justification=raggedright,singlelinecheck=true}
\\usepackage{capt-of} % required by Org
"
"Preamble that improves captions.")
(defvar org-latex-checkbox-preamble "
\\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$}
"
"Preamble that improves checkboxes.")
(defvar org-latex-box-preamble "
% args = #1 Name, #2 Colour, #3 Ding, #4 Label
\\newcommand{\\defsimplebox}[4]{%
\\definecolor{#1}{HTML}{#2}
\\newenvironment{#1}[1][]
{%
\\par\\vspace{-0.7\\baselineskip}%
\\textcolor{#1}{#3} \\textcolor{#1}{\\textbf{\\def\\temp{##1}\\ifx\\temp\\empty#4\\else##1\\fi}}%
\\vspace{-0.8\\baselineskip}
\\begin{addmargin}[1em]{1em}
}{%
\\end{addmargin}
\\vspace{-0.5\\baselineskip}
}%
}
"
"Preamble that provides a macro for custom boxes.")
#+end_src
** Conditional features
Don't always need everything in LaTeX, so only add it what we need when we need it.
#+begin_src emacs-lisp
(defvar org-latex-italic-quotes t
"Make \"quote\" environments italic.")
(defvar org-latex-par-sep t
"Vertically seperate paragraphs, and remove indentation.")
(defvar org-latex-conditional-features
'(("\\[\\[\\(?:file\\|https?\\):\\(?:[^]]\\|\\\\\\]\\)+?\\.\\(?:eps\\|pdf\\|png\\|jpeg\\|jpg\\|jbig2\\)\\]\\]" . image)
("\\[\\[\\(?:file\\|https?\\):\\(?:[^]]+?\\|\\\\\\]\\)\\.svg\\]\\]\\|\\\\includesvg" . svg)
("^[ \t]*|" . table)
("cref:\\|\\cref{\\|\\[\\[[^\\]]+\\]\\]" . cleveref)
("[;\\\\]?\\b[A-Z][A-Z]+s?[^A-Za-z]" . acronym)
("\\+[^ ].*[^ ]\\+\\|_[^ ].*[^ ]_\\|\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith" . underline)
(":float wrap" . float-wrap)
(":float sideways" . rotate)
("^[ \t]*#\\+caption:\\|\\\\caption" . caption)
("\\[\\[xkcd:" . (image caption))
((and org-latex-italic-quotes "^[ \t]*#\\+begin_quote\\|\\\\begin{quote}") . italic-quotes)
(org-latex-par-sep . par-sep)
("^[ \t]*\\(?:[-+*]\\|[0-9]+[.)]\\|[A-Za-z]+[.)]\\) \\[[ -X]\\]" . checkbox)
("^[ \t]*#\\+begin_warning\\|\\\\begin{warning}" . box-warning)
("^[ \t]*#\\+begin_info\\|\\\\begin{info}" . box-info)
("^[ \t]*#\\+begin_success\\|\\\\begin{success}" . box-success)
("^[ \t]*#\\+begin_error\\|\\\\begin{error}" . box-error))
"Org feature tests and associated LaTeX feature flags.
Alist where the car is a test for the presense of the feature,
and the cdr is either a single feature symbol or list of feature symbols.
When a string, it is used as a regex search in the buffer.
The feature is registered as present when there is a match.
The car can also be a
- symbol, the value of which is fetched
- function, which is called with info as an argument
- list, which is `eval'uated
If the symbol, function, or list produces a string: that is used as a regex
search in the buffer. Otherwise any non-nil return value will indicate the
existance of the feature.")
(defvar org-latex-feature-implementations
'((image :snippet "\\usepackage{graphicx}" :order 2)
(svg :snippet "\\usepackage{svg}" :order 2)
(table :snippet "\\usepackage{longtable}\n\\usepackage{booktabs}" :order 2)
(cleveref :snippet "\\usepackage[capitalize]{cleveref}" :order 1)
(underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
(float-wrap :snippet "\\usepackage{wrapfig}" :order 2)
(rotate :snippet "\\usepackage{rotating}" :order 2)
(caption :snippet org-latex-caption-preamble :order 2.1)
(acronym :snippet "\\newcommand{\\acr}[1]{\\protect\\textls*[110]{\\scshape #1}}\n\\newcommand{\\acrs}{\\protect\\scalebox{.91}[.84]{\\hspace{0.15ex}s}}" :order 0.4)
(italic-quotes :snippet "\\renewcommand{\\quote}{\\list{}{\\rightmargin\\leftmargin}\\item\\relax\\em}\n" :order 0.5)
(par-sep :snippet "\\setlength{\\parskip}{\\baselineskip}\n\\setlength{\\parindent}{0pt}\n" :order 0.5)
(.pifont :snippet "\\usepackage{pifont}")
(checkbox :requires .pifont :order 3
:snippet (concat (unless (memq 'maths features)
"\\usepackage{amssymb} % provides \\square")
org-latex-checkbox-preamble))
(.fancy-box :requires .pifont :snippet org-latex-box-preamble :order 3.9)
(box-warning :requires .fancy-box :snippet "\\defsimplebox{warning}{e66100}{\\ding{68}}{Warning}" :order 4)
(box-info :requires .fancy-box :snippet "\\defsimplebox{info}{3584e4}{\\ding{68}}{Information}" :order 4)
(box-success :requires .fancy-box :snippet "\\defsimplebox{success}{26a269}{\\ding{68}}{\\vspace{-\\baselineskip}}" :order 4)
(box-error :requires .fancy-box :snippet "\\defsimplebox{error}{c01c28}{\\ding{68}}{Important}" :order 4))
"LaTeX features and details required to implement them.
List where the car is the feature symbol, and the rest forms a plist with the
following keys:
- :snippet, which may be either
- a string which should be included in the preamble
- a symbol, the value of which is included in the preamble
- a function, which is evaluated with the list of feature flags as its
single argument. The result of which is included in the preamble
- a list, which is passed to `eval', with a list of feature flags available
as \"features\"
- :requires, a feature or list of features that must be available
- :when, a feature or list of features that when all available should cause this
to be automatically enabled.
- :prevents, a feature or list of features that should be masked
- :order, for when ordering is important. Lower values appear first.
The default is 0.
Features that start with ! will be eagerly loaded, i.e. without being detected.")
#+end_src
First, we need to detect which features we actually need
#+begin_src emacs-lisp
(defun org-latex-detect-features (&optional buffer info)
"List features from `org-latex-conditional-features' detected in BUFFER."
(let ((case-fold-search nil))
(with-current-buffer (or buffer (current-buffer))
(delete-dups
(mapcan (lambda (construct-feature)
(when (let ((out (pcase (car construct-feature)
((pred stringp) (car construct-feature))
((pred functionp) (funcall (car construct-feature) info))
((pred listp) (eval (car construct-feature)))
((pred symbolp) (symbol-value (car construct-feature)))
(_ (user-error "org-latex-conditional-features key %s unable to be used" (car construct-feature))))))
(if (stringp out)
(save-excursion
(goto-char (point-min))
(re-search-forward out nil t))
out))
(if (listp (cdr construct-feature)) (cdr construct-feature) (list (cdr construct-feature)))))
org-latex-conditional-features)))))
#+end_src
Then we need to expand them and sort them according to the above definitions
#+begin_src emacs-lisp
(defun org-latex-expand-features (features)
"For each feature in FEATURES process :requires, :when, and :prevents keywords and sort according to :order."
(dolist (feature features)
(unless (assoc feature org-latex-feature-implementations)
(error "Feature %s not provided in org-latex-feature-implementations" feature)))
(setq current features)
(while current
(when-let ((requirements (plist-get (cdr (assq (car current) org-latex-feature-implementations)) :requires)))
(setcdr current (if (listp requirements)
(append requirements (cdr current))
(cons requirements (cdr current)))))
(setq current (cdr current)))
(dolist (potential-feature
(append features (delq nil (mapcar (lambda (feat)
(when (plist-get (cdr feat) :eager)
(car feat)))
org-latex-feature-implementations))))
(when-let ((prerequisites (plist-get (cdr (assoc potential-feature org-latex-feature-implementations)) :when)))
(setf features (if (if (listp prerequisites)
(cl-every (lambda (preq) (memq preq features)) prerequisites)
(memq prerequisites features))
(append (list potential-feature) features)
(delq potential-feature features)))))
(dolist (feature features)
(when-let ((prevents (plist-get (cdr (assoc feature org-latex-feature-implementations)) :prevents)))
(setf features (cl-set-difference features (if (listp prevents) prevents (list prevents))))))
(sort (delete-dups features)
(lambda (feat1 feat2)
(if (< (or (plist-get (cdr (assoc feat1 org-latex-feature-implementations)) :order) 1)
(or (plist-get (cdr (assoc feat2 org-latex-feature-implementations)) :order) 1))
t nil))))
#+end_src
Finally, we can create the preamble to be inserted
#+begin_src emacs-lisp
(defun org-latex-generate-features-preamble (features)
"Generate the LaTeX preamble content required to provide FEATURES.
This is done according to `org-latex-feature-implementations'"
(let ((expanded-features (org-latex-expand-features features)))
(concat
(format "\n%% features: %s\n" expanded-features)
(mapconcat (lambda (feature)
(when-let ((snippet (plist-get (cdr (assoc feature org-latex-feature-implementations)) :snippet)))
(concat
(pcase snippet
((pred stringp) snippet)
((pred functionp) (funcall snippet features))
((pred listp) (eval `(let ((features ',features)) (,@snippet))))
((pred symbolp) (symbol-value snippet))
(_ (user-error "org-latex-feature-implementations :snippet value %s unable to be used" snippet)))
"\n")))
expanded-features
"")
"% end features\n")))
#+end_src
Last step, some advice to hook in all of the above to work
#+begin_src emacs-lisp
(defvar info--tmp nil)
(defadvice! org-latex-save-info (info &optional t_ s_)
:before #'org-latex-make-preamble
(setq info--tmp info))
(defadvice! org-splice-latex-header-and-generated-preamble-a (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
(org-latex-generate-features-preamble (org-latex-detect-features nil info--tmp))
"\n"))))
#+end_src
** Class templates
I really like the KOMA bundle. It provides a set of mechanisms to tweak document
styling which is both easy to use, and quite comprehensive.
For example, I rather like section numbers in the margin, which can be
accomplished with
#+begin_src emacs-lisp
(after! ox-latex
(let* ((article-sections '(("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(book-sections (append '(("\\chapter{%s}" . "\\chapter*{%s}"))
article-sections))
(hanging-secnum-preamble "
\\renewcommand\\sectionformat{\\llap{\\thesection\\autodot\\enskip}}
\\renewcommand\\subsectionformat{\\llap{\\thesubsection\\autodot\\enskip}}
\\renewcommand\\subsubsectionformat{\\llap{\\thesubsubsection\\autodot\\enskip}}
")
(big-chap-preamble "
\\RedeclareSectionCommand[afterindent=false, beforeskip=0pt, afterskip=0pt, innerskip=0pt]{chapter}
\\setkomafont{chapter}{\\normalfont\\Huge}
\\renewcommand*{\\chapterheadstartvskip}{\\vspace*{0\\baselineskip}}
\\renewcommand*{\\chapterheadendvskip}{\\vspace*{0\\baselineskip}}
\\renewcommand*{\\chapterformat}{%
\\fontsize{60}{30}\\selectfont\\rlap{\\hspace{6pt}\\thechapter}}
\\renewcommand*\\chapterlinesformat[3]{%
\\parbox[b]{\\dimexpr\\textwidth-0.5em\\relax}{%
\\raggedleft{{\\large\\scshape\\bfseries\\chapapp}\\vspace{-0.5ex}\\par\\Huge#3}}%
\\hfill\\makebox[0pt][l]{#2}}
"))
(setcdr (assoc "article" org-latex-classes)
`(,(concat "\\documentclass{scrartcl}" hanging-secnum-preamble)
,@article-sections))
(add-to-list 'org-latex-classes
`("report" ,(concat "\\documentclass{scrartcl}" hanging-secnum-preamble)
,@article-sections))
(add-to-list 'org-latex-classes
`("book" ,(concat "\\documentclass[twoside=false]{scrbook}"
big-chap-preamble hanging-secnum-preamble)
,@book-sections))
(add-to-list 'org-latex-classes
`("blank" "[NO-DEFAULT-PACKAGES]\n[NO-PACKAGES]\n[EXTRA]"
,@article-sections))
(add-to-list 'org-latex-classes
`("bmc-article" "\\documentclass[article,code,maths]{bmc}\n[NO-DEFAULT-PACKAGES]\n[NO-PACKAGES]\n[EXTRA]"
,@article-sections))
(add-to-list 'org-latex-classes
`("bmc" "\\documentclass[code,maths]{bmc}\n[NO-DEFAULT-PACKAGES]\n[NO-PACKAGES]\n[EXTRA]"
,@book-sections))))
(after! ox-latex
(setq org-latex-tables-booktabs t
org-latex-hyperref-template "\\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}
"
org-latex-reference-command "\\cref{%s}"))
#+end_src
** Adjust 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
(after! org
(setq org-latex-default-packages-alist
'(("AUTO" "inputenc" t ("pdflatex"))
("T1" "fontenc" t ("pdflatex"))
("" "fontspec" t)
("" "xcolor" nil)
("" "hyperref" nil)
("" "firamath-otf" t)
"\\setmonofont{Liga SFMono Nerd Font}"
"\\setmainfont{IBM Plex Sans}")))
#+end_src
** Cover page
To make a nice cover page, a simple method that comes to mind is just redefining
=\maketitle=. To get precise control over the positioning we'll use the =tikz=
package, and then add in the Tikz libraries =calc= and =shapes.geometric= to make
some nice decorations for the background.
I'll start off by setting up the required additions to the preamble.
This will accomplish the following:
- Load the required packages
- Redefine =\maketitle=
- Draw an Org icon with Tikz to use in the cover page (it's a little easter egg)
- Start a new page after the table of contents by redefining =\tableofcontents=
Now we've got a nice cover page to work with, we just need to use it every now
and then. Adding this to =#+options= feels most appropriate.
Let's have the =coverpage= option accept =auto= as a value and then decide whether
or not a cover page should be used based on the word count --- I'll have this be
the global default. Then we just want to insert a LaTeX snippet tweak the
subtitle format to use the cover page.
#+begin_src emacs-lisp
(after! org
(defvar org-latex-cover-page 'auto
"When t, use a cover page by default.
When auto, use a cover page when the document's wordcount exceeds
`org-latex-cover-page-wordcount-threshold'.
Set with #+option: coverpage:{yes,auto,no} in org buffers.")
(defvar org-latex-cover-page-wordcount-threshold 5000
"Document word count at which a cover page will be used automatically.
This condition is applied when cover page option is set to auto.")
(defvar org-latex-subtitle-coverpage-format "\\\\\\bigskip\n\\LARGE\\mdseries\\itshape\\color{black!80} %s\\par"
"Variant of `org-latex-subtitle-format' to use with the cover page.")
(defvar org-latex-cover-page-maketitle "
\\usepackage{tikz}
\\usetikzlibrary{shapes.geometric}
\\usetikzlibrary{calc}
\\newsavebox\\orgicon
\\begin{lrbox}{\\orgicon}
\\begin{tikzpicture}[y=0.80pt, x=0.80pt, inner sep=0pt, outer sep=0pt]
\\path[fill=black!6] (16.15,24.00) .. controls (15.58,24.00) and (13.99,20.69) .. (12.77,18.06)arc(215.55:180.20:2.19) .. controls (12.33,19.91) and (11.27,19.09) .. (11.43,18.05) .. controls (11.36,18.09) and (10.17,17.83) .. (10.17,17.82) .. controls (9.94,18.75) and (9.37,19.44) .. (9.02,18.39) .. controls (8.32,16.72) and (8.14,15.40) .. (9.13,13.80) .. controls (8.22,9.74) and (2.18,7.75) .. (2.81,4.47) .. controls (2.99,4.47) and (4.45,0.99) .. (9.15,2.41) .. controls (14.71,3.99) and (17.77,0.30) .. (18.13,0.04) .. controls (18.65,-0.49) and (16.78,4.61) .. (12.83,6.90) .. controls (10.49,8.18) and (11.96,10.38) .. (12.12,11.15) .. controls (12.12,11.15) and (14.00,9.84) .. (15.36,11.85) .. controls (16.58,11.53) and (17.40,12.07) .. (18.46,11.69) .. controls (19.10,11.41) and (21.79,11.58) .. (20.79,13.08) .. controls (20.79,13.08) and (21.71,13.90) .. (21.80,13.99) .. controls (21.97,14.75) and (21.59,14.91) .. (21.47,15.12) .. controls (21.44,15.60) and (21.04,15.79) .. (20.55,15.44) .. controls (19.45,15.64) and (18.36,15.55) .. (17.83,15.59) .. controls (16.65,15.76) and (15.67,16.38) .. (15.67,16.38) .. controls (15.40,17.19) and (14.82,17.01) .. (14.09,17.32) .. controls (14.70,18.69) and (14.76,19.32) .. (15.50,21.32) .. controls (15.76,22.37) and (16.54,24.00) .. (16.15,24.00) -- cycle(7.83,16.74) .. controls (6.83,15.71) and (5.72,15.70) .. (4.05,15.42) .. controls (2.75,15.19) and (0.39,12.97) .. (0.02,10.68) .. controls (-0.02,10.07) and (-0.06,8.50) .. (0.45,7.18) .. controls (0.94,6.05) and (1.27,5.45) .. (2.29,4.85) .. controls (1.41,8.02) and (7.59,10.18) .. (8.55,13.80) -- (8.55,13.80) .. controls (7.73,15.00) and (7.80,15.64) .. (7.83,16.74) -- cycle;
\\end{tikzpicture}
\\end{lrbox}
\\makeatletter
\\g@addto@macro\\tableofcontents{\\clearpage}
\\renewcommand\\maketitle{
\\thispagestyle{empty}
\\hyphenpenalty=10000 % hyphens look bad in titles
\\renewcommand{\\baselinestretch}{1.1}
\\let\\oldtoday\\today
\\renewcommand{\\today}{\\LARGE\\number\\year\\\\\\large%
\\ifcase \\month \\or Jan\\or Feb\\or Mar\\or Apr\\or May \\or Jun\\or Jul\\or Aug\\or Sep\\or Oct\\or Nov\\or Dec\\fi
~\\number\\day}
\\begin{tikzpicture}[remember picture,overlay]
%% Background Polygons %%
\\foreach \\i in {2.5,...,22} % bottom left
{\\node[rounded corners,black!3.5,draw,regular polygon,regular polygon sides=6, minimum size=\\i cm,ultra thick] at ($(current page.west)+(2.5,-4.2)$) {} ;}
\\foreach \\i in {0.5,...,22} % top left
{\\node[rounded corners,black!5,draw,regular polygon,regular polygon sides=6, minimum size=\\i cm,ultra thick] at ($(current page.north west)+(2.5,2)$) {} ;}
\\node[rounded corners,fill=black!4,regular polygon,regular polygon sides=6, minimum size=5.5 cm,ultra thick] at ($(current page.north west)+(2.5,2)$) {};
\\foreach \\i in {0.5,...,24} % top right
{\\node[rounded corners,black!2,draw,regular polygon,regular polygon sides=6, minimum size=\\i cm,ultra thick] at ($(current page.north east)+(0,-8.5)$) {} ;}
\\node[fill=black!3,rounded corners,regular polygon,regular polygon sides=6, minimum size=2.5 cm,ultra thick] at ($(current page.north east)+(0,-8.5)$) {};
\\foreach \\i in {21,...,3} % bottom right
{\\node[black!3,rounded corners,draw,regular polygon,regular polygon sides=6, minimum size=\\i cm,ultra thick] at ($(current page.south east)+(-1.5,0.75)$) {} ;}
\\node[fill=black!3,rounded corners,regular polygon,regular polygon sides=6, minimum size=2 cm,ultra thick] at ($(current page.south east)+(-1.5,0.75)$) {};
\\node[align=center, scale=1.4] at ($(current page.south east)+(-1.5,0.75)$) {\\usebox\\orgicon};
%% Text %%
\\node[left, align=right, black, text width=0.8\\paperwidth, minimum height=3cm, rounded corners,font=\\Huge\\bfseries] at ($(current page.north east)+(-2,-8.5)$)
{\\@title};
\\node[left, align=right, black, text width=0.8\\paperwidth, minimum height=2cm, rounded corners, font=\\Large] at ($(current page.north east)+(-2,-11.8)$)
{\\scshape \\@author};
\\renewcommand{\\baselinestretch}{0.75}
\\node[align=center,rounded corners,fill=black!3,text=black,regular polygon,regular polygon sides=6, minimum size=2.5 cm,inner sep=0, font=\\Large\\bfseries ] at ($(current page.west)+(2.5,-4.2)$)
{\\@date};
\\end{tikzpicture}
\\let\\today\\oldtoday
\\clearpage}
\\makeatother
"
"LaTeX snippet for the preamble that sets \\maketitle to produce a cover page.")
(after! ox
(eval '(cl-pushnew '(:latex-cover-page nil "coverpage" org-latex-cover-page)
(org-export-backend-options (org-export-get-backend 'latex)))))
(defun org-latex-cover-page-p ()
"Whether a cover page should be used when exporting this Org file."
(pcase (or (car
(delq nil
(mapcar
(lambda (opt-line)
(plist-get (org-export--parse-option-keyword opt-line 'latex) :latex-cover-page))
(cdar (org-collect-keywords '("OPTIONS"))))))
org-latex-cover-page)
((or 't 'yes) t)
('auto (when (> (count-words (point-min) (point-max)) org-latex-cover-page-wordcount-threshold) t))
(_ nil)))
(defadvice! org-latex-set-coverpage-subtitle-format-a (contents info)
"Set the subtitle format when a cover page is being used."
:before #'org-latex-template
(when (org-latex-cover-page-p)
(setf info (plist-put info :latex-subtitle-format org-latex-subtitle-coverpage-format))))
(add-to-list 'org-latex-feature-implementations '(cover-page :snippet org-latex-cover-page-maketitle :order 9) t)
(add-to-list 'org-latex-conditional-features '((org-latex-cover-page-p) . cover-page) t))
#+end_src
** Condensed lists
LaTeX is generally pretty good by default, but it's /really/ generous with how
much space it puts between list items by default. I'm generally not a fan.
Thankfully this is easy to correct with a small snippet:
#+begin_src emacs-lisp
(after! org
(defvar org-latex-condense-lists t
"Reduce the space between list items.")
(defvar org-latex-condensed-lists "
\\newcommand{\\setuplistspacing}{\\setlength{\\itemsep}{-0.5ex}\\setlength{\\parskip}{1.5ex}\\setlength{\\parsep}{0pt}}
\\let\\olditem\\itemize\\renewcommand{\\itemize}{\\olditem\\setuplistspacing}
\\let\\oldenum\\enumerate\\renewcommand{\\enumerate}{\\oldenum\\setuplistspacing}
\\let\\olddesc\\description\\renewcommand{\\description}{\\olddesc\\setuplistspacing}
")
(add-to-list 'org-latex-conditional-features '((and org-latex-condense-lists "^[ \t]*[-+]\\|^[ \t]*[1Aa][.)] ") . condensed-lists) t)
(add-to-list 'org-latex-feature-implementations '(condensed-lists :snippet org-latex-condensed-lists :order 0.7) t))
#+end_src
** Pretty code blocks
Ripped this off of Teco, thanks mate!
#+begin_src emacs-lisp
(use-package! engrave-faces-latex
:after ox-latex
:config
(setq org-latex-listings 'engraved))
#+end_src
#+begin_src emacs-lisp
(defvar-local org-export-has-code-p nil)
(defadvice! org-export-expect-no-code (&rest _)
:before #'org-export-as
(setq org-export-has-code-p nil))
(defadvice! org-export-register-code (&rest _)
:after #'org-latex-src-block
:after #'org-latex-inline-src-block-engraved
(setq org-export-has-code-p t))
(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
andn addition to the vastly superior visual output, this should also be much
faster to compile 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).
** Ox-chameleon
Match export with current emacs theme
#+begin_src emacs-lisp
(use-package! ox-chameleon
:after ox)
#+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
(after! org
(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
** Beamer Exports
I've also recently got into presenting more and more often, so its nice to have beamer do everything for us
#+begin_src emacs-lisp
(setq org-beamer-theme "[progressbar=foot]metropolis"
org-beamer-frame-level 2)
(defun org-beamer-p (info)
(eq 'beamer (and (plist-get info :back-end)
(org-export-backend-name (plist-get info :back-end)))))
(defvar org-beamer-metropolis-tweaks "
\\NewCommandCopy{\\moldusetheme}{\\usetheme}
\\renewcommand*{\\usetheme}[2][]{\\moldusetheme[#1]{#2}
\\setbeamertemplate{items}{$\\bullet$}
\\setbeamerfont{block title}{size=\\normalsize, series=\\bfseries\\parbox{0pt}{\\rule{0pt}{4ex}}}}
\\makeatletter
\\newcommand{\\setmetropolislinewidth}{
\\setlength{\\metropolis@progressinheadfoot@linewidth}{1.2px}}
\\makeatother
\\usepackage{etoolbox}
\\AtEndPreamble{\\setmetropolislinewidth}
")
(add-to-list 'org-latex-conditional-features '(org-beamer-p . beamer) t)
(add-to-list 'org-latex-feature-implementations '(beamer :requires .missing-koma :prevents (italic-quotes condensed-lists)) t)
(add-to-list 'org-latex-feature-implementations '(.missing-koma :snippet "\\usepackage{scrextend}" :order 2) t)
(add-to-list 'org-latex-conditional-features '((lambda (info) (and (org-beamer-p info) (string-match-p "metropolis$" org-beamer-theme))) . beamer-metropolis) t)
(add-to-list 'org-latex-feature-implementations '(beamer-metropolis :requires beamer :snippet org-beamer-metropolis-tweaks :order 3) t)
#+end_src