#+PROPERTY: header-args :tangle ./init.el
#+begin_quote
1. Clone this repo to ~/.emacs.d
2. Open =readme.org= in emacs
3. Press =C-c C-v t= to tangle all the blocks
4. Restart Emacs. Elpaca will now clone all the packages, and then
you'll be ready for a spooky Emacs.
#+end_quote
This setup make certain assumptions regarding its environment:
1. Machine in use is Linux or MacOS
2. Shell in use is =fish=
* Spook max 👻
:PROPERTIES:
:ID: D06520AB-2147-4FA7-83D4-6F3349B4743C
:END:
My [[https://github.com/progfolio/elpaca][Elpaca]] based emacs configuration.
- Bootstrap
#+begin_src elisp :tangle ./early-init.el
;; -*- lexical-binding: t -*-
(setq package-enable-at-startup nil
package-install-upgrade-built-in t)
;;
;;; Bootstrap
;; Contrary to what many Emacs users have in their configs, you don't need
;; more than this to make UTF-8 the default coding system:
(set-language-environment "UTF-8")
;; set-language-enviornment sets default-input-method, which is unwanted
(setq default-input-method nil)
#+end_src
** Initialize Elpaca itself
:PROPERTIES:
:ID: 635cd7c4-e3cb-4a0e-a722-6fa8f2035ea0
:END:
#+begin_src elisp :tangle ./init.el
;; -*- lexical-binding: t -*-
(defvar elpaca-installer-version 0.7)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil
:files (:defaults (:exclude "extensions"))
:build (:not elpaca--activate-package)))
(let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (< emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (call-process "git" nil buffer t "clone"
(plist-get order :repo) repo)))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
#+end_src
- Install =use-package=
#+begin_src elisp
;; Install use-package support
(elpaca elpaca-use-package
;; Enable :elpaca use-package keyword.
(elpaca-use-package-mode)
;; Assume :elpaca t unless otherwise specified.
(setq elpaca-use-package-by-default t))
(elpaca-wait)
#+end_src
** Helper utilities
:PROPERTIES:
:ID: 675D81B1-9A5A-44E8-BA29-888C967974F9
:END:
- Ergonomically create keymaps
#+begin_src elisp
(defmacro spook--defkeymap (name prefix &rest bindings)
"Create a new NAME-keymap bound to PREFIX, with BINDINGS.
Usage:
(spook--defkeymap \"spook-git\" \"C-c g\"
'(\"s\" . magit-status))
"
(let* ((keymap-name (intern (concat name "-keymap")))
(keymap-alias (intern name))
(keymap-bindings (mapcar
(lambda (binding)
(let ((binding (eval binding)))
`(define-key ,keymap-name (kbd ,(car binding)) #',(cdr binding))))
bindings)))
`(progn
(defvar ,keymap-name (make-sparse-keymap))
(defalias ',keymap-alias ,keymap-name)
(global-set-key (kbd ,prefix) ',keymap-alias)
,@keymap-bindings)))
#+end_src
- Utilities for helping with scratch buffer
This exactly function was introduced at or before Emacs version 29.1.
#+begin_src elisp
(defun spook--get-or-create-scratch ()
"Switch to *scratch* buffer. Create if it doesn't already exist"
(interactive)
(let ((s-buf (get-buffer "*scratch*")))
(switch-to-buffer (get-buffer-create "*scratch*"))
(when (not s-buf) (emacs-lisp-mode))))
#+end_src
For firefox, I want to capture whatever I am reading and open the captured
content in baby-window. But there is a fuck-up here that I've been unable to
fix. On aborting the capture, the window layout gets messed up.
#+begin_src elisp
(defun spook--baby-window (&optional baby-size)
"Create a baby of active window of BABY-SIZE height.
A baby-window is a small window below active-window, like
DevConsole in a browser. Depending on the active-window,
baby-window contains a different application. If the prefix-arg
is nil, baby-window always open *scratch* buffer."
(interactive)
(let ((baby-window (split-window-below (or baby-size -20))))
(select-window baby-window)
(spook--get-or-create-scratch)))
#+end_src
- Profiles
Let's introduce a concept of profiles to change the configuration based on
different scenarios. Right now I run my Emacs on two machines, but instead for
adding checks for which machine I am on right now, we'll create a default
configuration, and modify it based on which profiles are active right now. At
startup, we'll perform the checks to automatically enable certain profiles.
A profile is a cons cell of =(name . metadata)=
#+begin_src elisp
(defvar spook--active-profiles '()
"Change things slightly based on different profiles.")
#+end_src
- On mac without external monitor
#+begin_src elisp
(when (eq system-type 'darwin)
(push '(small-screen . t) spook--active-profiles))
#+end_src
** Preliminary setup
:PROPERTIES:
:ID: 704db7c8-f339-48cc-8e2c-d680da5899fd
:END:
- Start emacs as a server
#+begin_src elisp
(server-start)
#+end_src
- Unset annoying keybindings
#+begin_src elisp
(global-unset-key (kbd "C-x C-z"))
(global-unset-key (kbd "C-z"))
(global-unset-key (kbd "C-h h"))
#+end_src
- Set a custom-file so Emacs won't put customized entries in my =init.el= which
gets overwritten every time I tangle spookmax.d
#+BEGIN_SRC elisp
(setq custom-file (concat user-emacs-directory "custom.el"))
#+END_SRC
- Disable the ugly-ass toolbar, scroll-bars and menu-bar
#+begin_src elisp :tangle ./init.el
(setq inhibit-startup-screen t
ring-bell-function #'ignore
use-dialog-box nil)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(menu-bar-mode -1)
(tooltip-mode -1)
#+end_src
- Make emacs a little transparent
#+begin_src elisp :tangle ./init.el
(set-frame-parameter (selected-frame) 'alpha '(98 . 95))
(add-to-list 'default-frame-alist '(alpha . (98 . 95)))
#+end_src
- Disable native-comp warnings
#+begin_src elisp
(setq native-comp-async-report-warnings-errors 'silent)
#+end_src
- UI fixes copied from Doom
https://github.com/hlissner/doom-emacs/blob/develop/core/core-ui.el
- Scrolling
#+begin_src elisp
;;; Scrolling
(setq hscroll-margin 2
hscroll-step 1
;; Emacs spends too much effort recentering the screen if you scroll the
;; cursor more than N lines past window edges (where N is the settings of
;; `scroll-conservatively'). This is especially slow in larger files
;; during large-scale scrolling commands. If kept over 100, the window is
;; never automatically recentered.
scroll-conservatively 101
scroll-margin 0
scroll-preserve-screen-position t
;; Reduce cursor lag by a tiny bit by not auto-adjusting `window-vscroll'
;; for tall lines.
auto-window-vscroll nil
;; mouse
mouse-wheel-scroll-amount '(2 ((shift) . hscroll))
mouse-wheel-scroll-amount-horizontal 2)
#+end_src
- Cursors
#+begin_src elisp
;;; Cursor
(blink-cursor-mode -1)
;; Don't blink the paren matching the one at point, it's too distracting.
(setq blink-matching-paren nil)
;; Don't stretch the cursor to fit wide characters, it is disorienting,
;; especially for tabs.
(setq x-stretch-cursor nil)
#+end_src
- Window/Frame
#+begin_src elisp
;; A simple frame title
(setq frame-title-format '("%b")
icon-title-format frame-title-format)
;; Don't resize the frames in steps; it looks weird, especially in tiling window
;; managers, where it can leave unseemly gaps.
(setq frame-resize-pixelwise t)
;; But do not resize windows pixelwise, this can cause crashes in some cases
;; when resizing too many windows at once or rapidly.
(setq window-resize-pixelwise nil)
;; Favor vertical splits over horizontal ones. Monitors are trending toward
;; wide, rather than tall.
(setq split-width-threshold 160
split-height-threshold nil)
#+end_src
- Minibuffer
#+begin_src elisp
;;
;;; Minibuffer
;; Allow for minibuffer-ception. Sometimes we need another minibuffer command
;; while we're in the minibuffer.
(setq enable-recursive-minibuffers t)
;; Show current key-sequence in minibuffer ala 'set showcmd' in vim. Any
;; feedback after typing is better UX than no feedback at all.
(setq echo-keystrokes 0.02)
;; Expand the minibuffer to fit multi-line text displayed in the echo-area. This
;; doesn't look too great with direnv, however...
(setq resize-mini-windows 'grow-only)
;; Typing yes/no is obnoxious when y/n will do
(setf use-short-answers t)
;; Try to keep the cursor out of the read-only portions of the minibuffer.
(setq minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
;; Don't resize the frames in steps; it looks weird, especially in tiling window
;; managers, where it can leave unseemly gaps.
(setq frame-resize-pixelwise t)
;; But do not resize windows pixelwise, this can cause crashes in some cases
;; when resizing too many windows at once or rapidly.
(setq window-resize-pixelwise nil)
#+end_src
- Allow selection to be deleted, generally expected behavior during
editing. I tried to not have this on by default, but I am finding
that to be increasingly annoying.
#+begin_src elisp
(delete-selection-mode +1)
#+end_src
- Indentation and whitespace
#+begin_src elisp
(setq spook--indent-width 2)
(setq-default tab-width spook--indent-width)
(setq-default indent-tabs-mode nil)
#+end_src
From: https://github.com/susam/emfy/blob/main/.emacs#L26
#+begin_src elisp
(setq-default indicate-empty-lines t)
(setq-default indicate-buffer-boundaries 'left)
;; Consider a period followed by a single space to be end of sentence.
(setq sentence-end-double-space nil)
(setq create-lockfiles nil)
#+end_src
I got sick of manually calling whitespace cleanup all the trim.
Cleanup whitespace.
#+begin_src elisp
(use-package whitespace-cleanup-mode
:config
(global-whitespace-cleanup-mode +1))
#+end_src
- Fill column for auto-formatting/filling paragraphs.
#+begin_src elisp
(setq-default fill-column 100)
#+end_src
- Introspection
:PROPERTIES:
:ID: e17d83de-251c-4407-b2ea-ca9c428e5ea1
:END:
Setup =which-key= for easy keys discovery
#+begin_src elisp
(use-package which-key
:config
(which-key-mode t))
#+end_src
- Highlighting
:PROPERTIES:
:ID: 79c1e2a9-c52e-4660-ba70-f6f1f98f7d4e
:END:
#+begin_src elisp
(global-hl-line-mode +1)
(use-package highlight-symbol
:hook (prog-mode . highlight-symbol-mode)
:config
(setq highlight-symbol-idle-delay 0.3))
#+end_src
- Line numbers
:PROPERTIES:
:ID: 2b554619-a8c0-4bd0-8ab0-8107c52a6e7e
:END:
#+begin_src elisp
(global-display-line-numbers-mode 1)
#+end_src
- Window management
- Custom window keybindings
#+begin_src elisp
(spook--defkeymap "spook-windows" "C-c s-w"
'("-" . split-window-below)
'("_" . spook--baby-window)
'("/" . split-window-right)
'("d" . delete-window)
'("m" . delete-other-windows)
'("o" . other-window)
'("h" . windmove-left)
'("j" . windmove-down)
'("k" . windmove-up)
'("l" . windmove-right)
'("w" . ace-window))
#+end_src
- Install [[https://github.com/abo-abo/ace-window][ace-window]] for some nice utilities.
#+begin_src elisp
(defun spook--aw-kill-buffer-in-window (win)
"Kill the buffer shown in window WIN."
(kill-buffer (window-buffer win)))
(defun spook--aw-kill-buffer-and-window (win)
"Kill the buffer shown in window WIN and window itself."
(kill-buffer (window-buffer win))
(delete-window win))
(use-package ace-window
:config
(setq aw-dispatch-always t)
(global-set-key (kbd "C-c w") 'ace-window)
(setq aw-dispatch-alist
'((?d spook--aw-kill-buffer-in-window "Kill buffer in window")
(?s aw-swap-window "Swap Windows")
(?S aw-move-window "Move Window")
(?c aw-copy-window "Copy Window")
(?w aw-flip-window)
(?b aw-switch-buffer-in-window "Select Buffer")
(?B aw-switch-buffer-other-window "Switch Buffer Other Window")
(?k aw-delete-window "Delete Window")
(?K spook--aw-kill-buffer-and-window "Kill buffer in window")
(?= aw-split-window-fair "Split Fair Window")
(?- aw-split-window-vert "Split Vert Window")
(?/ aw-split-window-horz "Split Horz Window")
(?m delete-other-windows "Delete Other Windows")
(?? aw-show-dispatch-help))
aw-keys '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
#+end_src
- Workspace management with perspective
I was using eyebrowse earlier, but I don't like its reliance on desktop-mode
to save state. Let's give perspective a shot
#+begin_src elisp
(use-package perspective
:init
(setq persp-mode-prefix-key (kbd "C-c C-w"))
:config
(persp-mode +1))
#+end_src
- Buffer management
#+begin_src elisp
(spook--defkeymap
"spook-buffers" "C-c b"
'("b" . switch-to-buffer)
'("n" . next-buffer)
'("p" . previous-buffer)
'("n" . next-buffer)
'("d" . kill-current-buffer)
'("s" . spook--get-or-create-scratch))
#+end_src
- Font size
#+begin_src elisp
(defvar spook--font-size 11)
(when (assoc 'small-screen spook--active-profiles)
(setq spook--font-size 14))
(set-face-attribute 'default nil :height (* 10 spook--font-size))
#+end_src
- [Ma]git
Magit uses =project-switch-commands= which are present only in more
recent project.el project.
#+begin_src elisp
(use-package project)
#+end_src
#+begin_src elisp
(use-package transient
:ensure (transient :host github :repo "magit/transient" :branch "main"))
(use-package magit
:ensure (magit :host github :repo "magit/magit" :branch "main")
:config
(setq magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1
magit-bury-buffer-function #'magit-restore-window-configuration))
#+end_src
- [[https://github.com/sshaw/git-link][git-link]] so I can copy link to lines in files because evidently, I am doing that a lot
#+begin_src emacs-lisp
(use-package git-link
:config
(setf git-link-use-commit t))
#+end_src
- Buncha nice keybindings.
#+begin_src elisp
(spook--defkeymap "spook-git" "C-c g"
'("s" . magit-status)
'("b" . magit-blame)
'("g" . magit-dispatch))
#+end_src
- Magit Forge
#+begin_src elisp
;; (use-package forge
;; :after magit)
#+end_src
- Keep backup/auto-save files out of my vc
#+begin_src elisp
(setq
backup-dir "~/.emacs.d/bakups"
backup-directory-alist `((".*" . ,backup-dir))
auto-save-file-name-transforms `((".*" ,backup-dir t))
create-lockfiles nil)
#+end_src
- Setup PATH from shell
#+begin_src elisp
(use-package exec-path-from-shell
:config
(exec-path-from-shell-initialize))
#+end_src
** Org mode
:PROPERTIES:
:ID: 8b2528d8-3fd2-4076-8b1e-791df8ed9a67
:END:
- Install latest org-mode. Elpaca will install the latest org-mode, instead
of older version pre-packaged with emacs
#+begin_src elisp
(use-package org
:config
(eval-after-load 'org-mode
(org-link-set-parameters
"yt"
:follow #'spook-org--follow-yt-link
:export #'spook-org--export-yt-link))
(add-hook
'org-mode-hook
(lambda () (display-line-numbers-mode -1))))
#+end_src
- Other settings
#+begin_src elisp
(setq
org-startup-indented t
org-startup-folded t
org-agenda-window-setup "only-window"
org-directory "~/Documents/org"
org-agenda-diary-file (concat org-directory "/diary.org.gpg")
org-inbox-file (concat org-directory "/TODOs.org")
org-agenda-files (list org-inbox-file (expand-file-name "work/on.org" org-directory))
;;Todo keywords I need
org-todo-keywords '((sequence "TODO(t)" "DOING(n)" "|" "DONE(d)" "CANCELED(c@)"))
org-todo-keyword-faces '(("DOING" . "DeepSkyBlue")
("CANCELED" . org-done))
org-default-notes-file (concat org-directory "/refile.org")
org-refile-targets '((org-agenda-files . (:maxlevel . 6)))
org-capture-templates
'(("i" "Idea" entry (file+headline org-inbox-file "Inbox") "* %?\t\t:idea:\n%U")
("t" "Todo" entry (file+headline org-inbox-file "Inbox") "* TODO %?\n%U\n[[%F]]"))
org-log-into-drawer "LOGBOOK"
org-log-done "time"
org-clock-report-include-clocking-task t
org-clock-into-drawer t
org-fontify-done-headline t
org-enforce-todo-dependencies t
org-agenda-overriding-columns-format "%80ITEM(Task) %6Effort(XP){+}"
org-columns-default-format org-agenda-overriding-columns-format
org-use-property-inheritance t
org-confirm-babel-evaluate nil
org-id-link-to-org-use-id t
org-fold-catch-invisible-edits 'show
org-cycle-separator-lines 0
org-export-allow-bind-keywords t)
;; org-mode settings
(with-eval-after-load 'org
(org-indent-mode t)
(require 'org-id))
#+end_src
- Keybindings
#+begin_src elisp
(global-set-key (kbd "C-c c") #'org-capture)
(spook--defkeymap
"spook-org" "C-c o"
'("a" . org-agenda-list)
'("A" . org-agenda)
'("c" . org-capture)
'("C" . org-clock-goto)
'("o" . consult-org-agenda))
#+end_src
- org-super-agenda
:PROPERTIES:
:ID: 06dd246b-30f0-4c17-ab47-8128d49f7f69
:END:
More/better structure in agenda view.
#+begin_src elisp
(use-package org-super-agenda
:config
(org-super-agenda-mode t)
(setq org-super-agenda-groups
'((:name "Work" :tag "work" :order 1)
(:name "In Progress" :todo "DOING" :order 1)
(:name "Projects" :tag "project" :order 3)
(:name "Home" :tag "home" :order 2)
(:name "Study" :tag "study" :order 4)
(:name "Inbox" :tag "inbox" :order 4)
(:name "Habits" :tag "habit" :order 5))))
#+end_src
- org-babel
#+begin_src elisp
(use-package ob-http)
(with-eval-after-load 'org
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(plantuml . t)
(shell . t)
(sql . t)
(sqlite . t)
(lisp . t)
(js . t)
(http . t))))
#+end_src
- Allow adding HTML class/id to exported src blocks
Org mode don't allow adding custom HTML class or id to exported src
blocks, but I've found myself in need of this functionality when
customizing published projects.
#+begin_src elisp
(defun spook--org-src-block-html-attrs-advice (oldfun src-block contents info)
"Add class, id or data-* CSS attributes to html source block output.
Allows class, id or data attributes to be added to a source block using
#attr_html:
,#+ATTR_HTML: :class myclass :id myid
,#+begin_src python
print(\"Hi\")
,#+end_src
"
(let* ((old-ret (funcall oldfun src-block contents info))
(class-tag (org-export-read-attribute :attr_html src-block :class))
(data-attr (let ((attr (org-export-read-attribute :attr_html src-block :data)))
(when attr (split-string attr "="))))
(id-tag (org-export-read-attribute :attr_html src-block :id)))
(if (or class-tag id-tag data-attr)
(concat
"
"
old-ret
"
")
old-ret)))
(advice-add 'org-html-src-block :around #'spook--org-src-block-html-attrs-advice)
#+end_src
- Support exporting code blocks with syntax-highlighting
#+begin_src elisp
(use-package htmlize)
#+end_src
- Custom links
- =yt://= links
- Open =yt://= links in =mpv= if mpv is present
- Open =yt://= links in browser if mpv isn't installed or prefix-argument is
provided with =org-open-at-point= (i.e =C-c C-o=)
#+begin_src elisp
(defun spook-org--follow-yt-link (path prefix)
(let* ((url (format "https:%s" path))
(proc-name (format "*yt://%s*" url)))
(if (and prefix (executable-find "mpv"))
(browse-url url)
(make-process :name proc-name :buffer proc-name :command `("mpv" ,url))
(message "Launched mpv in buffer: %s" proc-name))))
(defun spook-org--export-yt-link (path desc backend)
(when (eq backend 'html)
(let* ((video-id (cadar (url-parse-query-string path)))
(url (if (string-empty-p video-id) path
(format "//youtube.com/embed/%s" video-id))))
(format
""
url desc))))
#+end_src
** Modal editing with Meow
:PROPERTIES:
:ID: 17c2eeec-133f-49f3-b2ce-95bf3dab1188
:END:
Let's get some modal editing with some spice. I have used Evil mode
with Spacemacs, I was going to configure Evil, but let's give meow a
shot!
- Meow qwerty setup copied from https://github.com/meow-edit/meow/blob/master/KEYBINDING_QWERTY.org
#+begin_src elisp
(defun meow-setup ()
(setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
(meow-motion-overwrite-define-key
'("j" . meow-next)
'("k" . meow-prev)
'("" . ignore))
(meow-leader-define-key
;; SPC j/k will run the original command in MOTION state.
'("j" . "H-j")
'("k" . "H-k")
;; Use SPC (0-9) for digit arguments.
'("1" . meow-digit-argument)
'("2" . meow-digit-argument)
'("3" . meow-digit-argument)
'("4" . meow-digit-argument)
'("5" . meow-digit-argument)
'("6" . meow-digit-argument)
'("7" . meow-digit-argument)
'("8" . meow-digit-argument)
'("9" . meow-digit-argument)
'("0" . meow-digit-argument)
;; '("/" . meow-keypad-describe-key)
'("?" . meow-cheatsheet))
(meow-normal-define-key
'("0" . meow-expand-0)
'("9" . meow-expand-9)
'("8" . meow-expand-8)
'("7" . meow-expand-7)
'("6" . meow-expand-6)
'("5" . meow-expand-5)
'("4" . meow-expand-4)
'("3" . meow-expand-3)
'("2" . meow-expand-2)
'("1" . meow-expand-1)
'("-" . negative-argument)
'(";" . meow-reverse)
'("," . meow-inner-of-thing)
'("." . meow-bounds-of-thing)
'("[" . meow-beginning-of-thing)
'("]" . meow-end-of-thing)
'("a" . meow-append)
'("A" . meow-open-below)
'("b" . meow-back-word)
'("B" . meow-back-symbol)
'("c" . meow-change)
'("d" . meow-delete)
'("D" . meow-backward-delete)
'("e" . meow-next-word)
'("E" . meow-next-symbol)
'("f" . meow-find)
'("g" . meow-cancel-selection)
'("G" . meow-grab)
'("h" . meow-left)
'("H" . meow-left-expand)
'("i" . meow-insert)
'("I" . meow-open-above)
'("j" . meow-next)
'("J" . meow-next-expand)
'("k" . meow-prev)
'("K" . meow-prev-expand)
'("l" . meow-right)
'("L" . meow-right-expand)
'("m" . meow-join)
'("n" . meow-search)
'("o" . meow-block)
'("O" . meow-to-block)
'("p" . meow-yank)
;; '("q" . meow-quit)
;; '("Q" . meow-goto-line)
'("r" . meow-replace)
'("R" . meow-swap-grab)
'("s" . meow-kill)
'("t" . meow-till)
'("u" . meow-undo)
'("U" . meow-undo-in-selection)
'("v" . meow-visit)
'("w" . meow-mark-word)
'("W" . meow-mark-symbol)
'("x" . meow-line)
;; '("X" . meow-goto-line)
'("y" . meow-save)
'("Y" . meow-sync-grab)
'("z" . meow-pop-selection)
'("'" . repeat)
'("" . ignore)))
#+end_src
#+begin_src elisp
(use-package meow
:config
(setf meow-use-clipboard t)
(meow-global-mode)
(meow-setup))
(elpaca-wait)
#+end_src
- Normal mode-keybindings. Mostly mimicking vim
#+begin_src elisp
(meow-normal-define-key
'("z" . spook-fold)
'("/" . "C-s")
'("?" . "C-r"))
#+end_src
- Leader keybindings
#+begin_src elisp
(meow-leader-define-key
'("/" . consult-git-grep)
'("p" . projectile-command-map)
'("e" . flycheck-command-map)
'("w" . ace-window)
'("b" . spook-buffers)
'("G" . spook-git)
'("o" . spook-org)
'("n" . spook-notes))
#+end_src
- Keychords
#+begin_src elisp
(use-package key-chord
:config
(setf key-chord-two-keys-delay 0.1)
(key-chord-mode 1)
(key-chord-define meow-insert-state-keymap "fd" #'meow-insert-exit))
#+end_src
** Completion UI
:PROPERTIES:
:ID: 4b16f866-dede-4d72-8fbf-95044ed1e378
:END:
- Orderlies adds matches completion candidates by space-separated patterns in
any order
#+begin_src elisp
(use-package orderless
:config
(setq completion-styles '(orderless)))
#+end_src
- Vertico for completion UI
#+begin_src elisp
(use-package vertico
:ensure (:files (:defaults "extensions/*.el"))
:init (vertico-mode +1)
:config
(define-key vertico-map (kbd "C-c ?") #'minibuffer-completion-help))
(use-package vertico-directory
:after vertico
:ensure nil
;; More convenient directory navigation commands
:bind (:map vertico-map
("C-h" . vertico-directory-delete-word))
;; Tidy shadowed file names
:hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
(use-package vertico-quick
:after vertico
:ensure nil
:bind (:map vertico-map
("C-q" . vertico-quick-insert))
;; Tidy shadowed file names
:hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
;; Persist history over Emacs restarts. Vertico sorts by history position.
(use-package savehist
:ensure nil
:init
(savehist-mode +1))
;; Emacs 28: Hide commands in M-x which do not work in the current mode.
;; Vertico commands are hidden in normal buffers.
(setq read-extended-command-predicate
#'command-completion-default-include-p)
#+end_src
- Marginalia adds pretty information to completions. It's pretty, useful, and
recommended by =embark= (it provides extra information to =embark=)
#+begin_src elisp
;; Enable richer annotations using the Marginalia package
(use-package marginalia
:bind (:map minibuffer-local-map
("M-A" . marginalia-cycle))
:init (marginalia-mode +1))
#+end_src
- Consult for enhanced commands
#+begin_src elisp
(use-package consult
:init
(setq consult-project-root-function #'projectile-project-root)
:config
(consult-customize consult-theme :preview-key '(:debounce 0.5 any))
(global-set-key (kbd "C-s") #'consult-line)
(global-set-key (kbd "C-r") #'consult-line-multi)
(global-set-key (kbd "C-x b") #'consult-buffer)
(define-key spook-buffers-keymap (kbd "b") #'consult-buffer)
(define-key spook-buffers-keymap (kbd "B") #'consult-buffer-other-window)
;; better yank which show kill-ring for selection
(global-set-key (kbd "C-y") #'consult-yank-pop)
(meow-leader-define-key
'("/" . consult-ripgrep))
(meow-normal-define-key
'("p" . consult-yank-pop)
'("Q" . consult-goto-line)
'("X" . consult-focus-lines)))
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref)
(recentf-mode +1)
(use-package consult-flycheck
:config
(define-key flycheck-command-map (kbd "l") #'consult-flycheck))
(use-package embark-consult
:after (embark consult)
:demand t
:hook
(embark-collect-mode . consult-preview-at-point-mode))
#+end_src
** Contextual actions
:PROPERTIES:
:ID: 8AF4F0B4-6688-439D-87D8-A70FD320B547
:END:
- [[https://github.com/oantolin/embark][embark]] allow contextual actions, like opening buffers in other window from
minibuffer and a lot more
#+begin_src elisp
(defun spook--embark-act-no-quit ()
"(embark-act), but don't quit the minibuffer"
(interactive)
(let ((embark-quit-after-action nil))
(embark-act)))
(use-package embark
:bind
(("C-," . embark-act)
("C-." . embark-dwim)
("C-h b" . embark-bindings)
("C-<" . spook--embark-act-no-quit)))
#+end_src
** More powerful editing
:PROPERTIES:
:ID: 968A1471-8B1D-4DD9-B871-E5B44EE2EF69
:END:
- =wgrep= for editing grep buffers
#+begin_src elisp
(use-package wgrep)
#+end_src
- =undo-tree-mode= for more powerful undo
#+begin_src elisp
(use-package undo-tree
:config
(global-undo-tree-mode t)
(global-set-key (kbd "C-/") #'undo)
(global-set-key (kbd "C-S-/") #'undo-tree-redo)
(setq undo-tree-history-directory-alist `(("." . ,(expand-file-name ".cache" user-emacs-directory)))))
#+end_src
- =embrace= for wrapping pair manipulation
#+begin_src elisp
(use-package embrace
:config
(add-hook 'org-mode-hook 'embrace-org-mode-hook)
(meow-normal-define-key
'("S" . embrace-commander)))
#+end_src
- [[https://github.com/joaotavora/yasnippet][yasnippet]] for templates
#+begin_src emacs-lisp
(use-package yasnippet
:config
(add-hook 'prog-mode-hook #'yas-minor-mode))
(use-package yasnippet-snippets
:after yasnippet)
#+end_src
** Programming
:PROPERTIES:
:ID: f88fd5b1-1170-43e3-b2b9-e3060edd7442
:END:
- Show trailing whitespace in programming files
#+begin_src elisp
(add-hook 'prog-mode-hook #'(lambda () (setq-local show-trailing-whitespace t)))
#+end_src
- Wrapping text in parens, quotes etc
#+begin_src elisp
(show-paren-mode 1)
(electric-pair-mode 1)
#+end_src
- Code folding
#+begin_src elisp
(spook--defkeymap
"spook-fold" "C-c f"
'("b" . hs-hide-block)
'("O" . hs-show-block)
'("l" . hs-hide-level)
'("L" . hs-show-block)
'("a" . hs-hide-all)
'("A" . hs-show-all)
'("z" . hs-toggle-hiding))
(add-hook 'prog-mode-hook 'hs-minor-mode)
#+end_src
- Flycheck for getting those in-buffer warnings errors.
#+begin_src elisp
(use-package flycheck
:init
(global-flycheck-mode t)
;; alias is needed for using the keymap in meow
(defalias 'flycheck-command-map flycheck-command-map))
#+end_src
Flycheck eglot integration
#+begin_src elisp
(use-package flycheck-eglot
:ensure t
:after (flycheck eglot)
:config
(global-flycheck-eglot-mode 1))
#+end_src
- Projectile for managing projects.
#+begin_src elisp
(use-package projectile
:init (projectile-mode +1)
:bind (:map projectile-mode-map
("s-p" . projectile-command-map)
("C-c p" . projectile-command-map)))
#+end_src
- Company mode
I think I have a general idea of what it does, but still fuzzy on
details. This stuff is usually taken for granted; I've been taking
it for granted with Spacemacs for a while now I suppose.
#+begin_src elisp
(use-package company
:init (global-company-mode +1))
#+end_src
[[https://github.com/sebastiencs/company-box/][company-box-mode]] adds icons and colors to company options.
#+begin_src elisp
(use-package company-box
:hook (company-mode . company-box-mode))
#+end_src
- [[https://github.com/purcell/emacs-reformatter][Reformatter]] allow creating buffer/region formatters from any command.
#+begin_src elisp
(use-package reformatter
:config
(reformatter-define prettier-format
:program (expand-file-name "node_modules/.bin/prettier"
(locate-dominating-file (buffer-file-name) "node_modules/.bin/prettier"))
:args `("--stdin-filepath" ,(buffer-file-name)))
:hook ((web-mode . prettier-format-on-save-mode)
(typescript-ts-mode . prettier-format-on-save-mode)))
#+end_src
- Direnv is pretty essential for my dev workflow.
#+begin_src elisp
(use-package direnv
:config
(direnv-mode)
(when (not (boundp 'warning-suppress-types))
(setq warning-suppress-types nil))
(add-to-list 'warning-suppress-types '(direnv)))
#+end_src
- Eglot to provide LSP support.
#+begin_src elisp
;; Need to be manuall installed so we get latest version
;; (use-package jsonrpc
;; :ensure '(jsonrpc :repo "https://git.savannah.gnu.org/git/emacs.git"
;; :files ("lisp/jsonrpc.el")))
;; (use-package eldoc
;; :ensure '(eldoc :repo "https://git.savannah.gnu.org/git/emacs.git"
;; :files ("lisp/emacs-lisp/eldoc.el")))
;; (use-package eglot)
;; Looks like jsonrpc logging make eglot super laggy for typescript.
;; https://old.reddit.com/r/emacs/comments/1447fy2/looking_for_help_in_improving_typescript_eglot/
;; https://www.reddit.com/r/emacs/comments/16vixg6/how_to_make_lsp_and_eglot_way_faster_like_neovim/
(fset #'jsonrpc--log-event #'ignore)
(setq eglot-events-buffer-size 0
eglot-sync-connect nil
eglot-connect-timeout nil
company-idle-delay 0
company-minimum-prefix-length 1)
(add-hook 'focus-out-hook 'garbage-collect)
#+end_src
#+RESULTS:
| garbage-collect |
*** Treesitter
#+begin_src elisp
(setq treesit-language-source-alist
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
(cmake "https://github.com/uyha/tree-sitter-cmake")
(css "https://github.com/tree-sitter/tree-sitter-css")
(elisp "https://github.com/Wilfred/tree-sitter-elisp")
(go "https://github.com/tree-sitter/tree-sitter-go")
(html "https://github.com/tree-sitter/tree-sitter-html")
(javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
(json "https://github.com/tree-sitter/tree-sitter-json")
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
(python "https://github.com/tree-sitter/tree-sitter-python")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/ikatyang/tree-sitter-yaml")))
#+end_src
When in NixOS, use system installed grammars.
#+begin_src elisp
(defvar nixos-p
(s-contains-p "NixOS" (shell-command-to-string "uname -a")))
(when nixos-p
(let ((nix-treesit-lib-path
(expand-file-name
"lib"
(string-replace
"\"" ""
(string-trim
(shell-command-to-string
"nix eval nixpkgs#emacsPackages.treesit-grammars.with-all-grammars.outPath"))))))
(setf treesit-extra-load-path (list nix-treesit-lib-path))))
#+end_src
- Structured movement
#+begin_src elisp
(use-package combobulate
:ensure (combobulate :host github :repo "mickeynp/combobulate")
:preface
;; You can customize Combobulate's key prefix here.
;; Note that you may have to restart Emacs for this to take effect!
(setq combobulate-key-prefix "C-c o")
;; Optional, but recommended.
;;
;; You can manually enable Combobulate with `M-x
;; combobulate-mode'.
:hook
((python-ts-mode . combobulate-mode)
(js-ts-mode . combobulate-mode)
(html-ts-mode . combobulate-mode)
(css-ts-mode . combobulate-mode)
(yaml-ts-mode . combobulate-mode)
(typescript-ts-mode . combobulate-mode)
(json-ts-mode . combobulate-mode)
(tsx-ts-mode . combobulate-mode)))
#+end_src
*** Lisp
:PROPERTIES:
:ID: 828dd6e7-a386-415c-b4e1-cb5515138109
:END:
Lispy for some nasty lisp structural editing.
#+begin_src elisp
(use-package lispy
:hook ((emacs-lisp-mode . lispy-mode)
(lisp-mode . lispy-mode))
:config
(setf lispy-colon-p nil))
#+end_src
Elsa provides very nice static-analysis and more for elisp
programming. First time I am trying this, hopefully it does what it
says on the box without much fuss.
#+begin_src elisp
(use-package flycheck-elsa
:after elsa
:hook (emacs-lisp-mode . flycheck-elsa-setup))
#+end_src
- Common Lisp
Sly for interactive development.
#+begin_src elisp
(use-package sly
:hook ((lisp-mode . sly-mode))
:config
(setq org-babel-lisp-eval-fn #'sly-eval
inferior-lisp-program "sbcl")
(add-hook
'sly-mrepl-hook
(lambda () (set-face-foreground 'sly-mrepl-output-face "khaki3"))))
#+end_src
[[https://github.com/mmgeorge/sly-asdf][sly-asdf]] add asdf integration to sly.
#+begin_src elisp
(use-package sly-asdf
:config
(add-to-list 'sly-contribs 'sly-asdf 'append))
#+end_src
*** Nix
:PROPERTIES:
:ID: 54D0A9B3-1F7D-4E39-BBAD-E2266930C489
:END:
#+begin_src elisp
(use-package nix-mode
:mode "\\.nix\\'")
#+end_src
*** Web dev
:PROPERTIES:
:ID: 62e08f15-d996-48fd-90c3-fd6d348555be
:END:
- Helper utilities
- Are we using nvm?
#+begin_src elisp
(defun spook--nvm-p ()
(when-let* ((node (string-trim (shell-command-to-string "fish -c 'readlink (which node)'")))
(nvm-bin-dir
(and (string-match-p "\/nvm\/" node)
(file-name-directory node))))
nvm-bin-dir))
#+end_src
#+begin_src elisp
(setq css-indent-offset spook--indent-width)
(use-package js-ts
:mode "\\.js'"
:ensure nil
:config
(setq js-indent-level spook--indent-width)
:hook
(((js-ts-mode
typescript-ts-mode) . subword-mode)))
(use-package web-mode
:mode (("\\.html?\\'" . web-mode))
:config
(setq web-mode-markup-indent-offset spook--indent-width)
(setq web-mode-code-indent-offset spook--indent-width)
(setq web-mode-css-indent-offset spook--indent-width)
(setq web-mode-content-types-alist '(("jsx" . "\\.js[x]?\\'"))))
(use-package emmet-mode
:hook ((html-mode . emmet-mode)
(css-mode . emmet-mode)
(js-ts-mode . emmet-mode)
(js-jsx-mode . emmet-mode)
(typescript-ts-mode . emmet-mode)
(web-mode . emmet-mode))
:config
(setq emmet-insert-flash-time 0.001) ; effectively disabling it
(add-hook 'js-jsx-mode-hook #'(lambda ()
(setq-local emmet-expand-jsx-className? t)))
(add-hook 'web-mode-hook #'(lambda ()
(setq-local emmet-expand-jsx-className? t))))
(spook--defkeymap
"spook-errors" "C-c e"
'("n" . flycheck-next-error)
'("p" . flycheck-previous-error)
'("l" . flycheck-list-errors)
'("e" . flycheck-explain-error-at-point))
(defun spook--setup-ts-js ()
"Setup Javascript and Typescript for current buffer."
;; Add node_modules/.bin of current project to exec-path.
(if-let (nvm-bin (spook--nvm-p))
(add-to-list 'exec-path nvm-bin)
(let ((bin-dir
(expand-file-name
"node_modules/.bin/"
(locate-dominating-file default-directory "node_modules"))))
(when (file-exists-p bin-dir)
(add-to-list 'exec-path bin-dir))))
;; For 95% of cases this is what I want
(prettier-format-on-save-mode +1)
(eglot-ensure)
(setf flymake-eslint-project-root
(locate-dominating-file default-directory "package.json")))
(add-hook 'js-ts-mode-hook #'spook--setup-ts-js)
(use-package typescript-ts-mode
:mode "\\.ts\\'"
:ensure nil
:hook ((typescript-ts-mode . subword-mode))
:config
(setq-default typescript-indent-level spook--indent-width)
(add-hook 'typescript-mode-hook #'spook--setup-ts-js))
(use-package css-ts-mode
:ensure nil
:mode "\\.s?css\\'")
#+end_src
- JSON support
#+begin_src elisp
(use-package json-ts-mode
:ensure nil
:mode "\\.json\\'")
#+end_src
- Testing with jest
#+begin_src elisp
(use-package jest-test-mode
:ensure t
:commands jest-test-mode
:hook (typescript-ts-mode)
:config
(setf jest-test-command-string "npm run test:only -- %s %s")
(define-key typescript-ts-mode-map (kbd "C-c , t r") #'jest-test-run)
(define-key typescript-ts-mode-map (kbd "C-c , t R") #'jest-test-rerun-test)
(define-key typescript-ts-mode-map (kbd "C-c , t t") #'jest-test-run-at-point)
(define-key typescript-ts-mode-map (kbd "C-c , t T") #'jest-test-debug-run-at-point)
(define-key typescript-ts-mode-map (kbd "C-c , t d") #'jest-test-debug))
#+end_src
*** Rust
:PROPERTIES:
:ID: 9A781996-6C6F-439D-B75D-E9F05BAD99F0
:END:
#+begin_src elisp
(use-package rustic
:init
(setq rustic-cargo-bin "cargo")
(push 'rustic-clippy flycheck-checkers))
#+end_src
*** Haskell
:PROPERTIES:
:ID: 3720944C-C928-4ECF-9B35-7620EEF7C682
:END:
#+begin_src elisp
(use-package haskell-mode
:mode "\\.hs\\'"
:config
(add-hook 'haskell-mode-hook #'subword-mode)
(define-key haskell-mode-map (kbd "C-c , c") #'haskell-process-load-or-reload)
(define-key haskell-mode-map (kbd "C-c , s") #'haskell-interactive-switch)
(define-key haskell-mode-map (kbd "C-c , l") #'haskell-interactive-mode-clear)
(define-key haskell-mode-map (kbd "C-c , T") #'haskell-doc-show-type)
(define-key haskell-mode-map (kbd "C-c , t") #'haskell-mode-show-type-at))
#+end_src
*** Yaml
:PROPERTIES:
:ID: C204CCCB-D27E-48F2-BEBB-A9A913C763A4
:END:
#+begin_src elisp
(use-package yaml-ts-mode
:ensure nil
:mode "\\.ya?ml\\'")
#+end_src
*** Graphql
:PROPERTIES:
:ID: 204E2F4A-9F0A-4458-8135-DD3861052AE3
:END:
#+begin_src elisp
(use-package graphql-mode
:mode "\\.graphql\\'")
#+end_src
** Niceties
:PROPERTIES:
:ID: 8e8563f8-2161-4af3-b072-fc3b81cc57a6
:END:
Nice to have features but not necessary.
- Ace Jump for quickly jumping around in a buffer
#+begin_src elisp
(spook--defkeymap
"spook-jump" "C-c q"
'("q" . ace-jump-mode)
'("w" . ace-jump-word-mode))
(use-package ace-jump-mode)
#+end_src
- Treemacs for easy code exploration
#+begin_src elisp
(use-package treemacs
:ensure t
:defer t
:init
(with-eval-after-load 'winum
(define-key winum-keymap (kbd "M-0") #'treemacs-select-window))
:config
(progn
(setq treemacs-collapse-dirs (if treemacs-python-executable 3 0)
treemacs-deferred-git-apply-delay 0.5
treemacs-directory-name-transformer #'identity
treemacs-display-in-side-window t
treemacs-eldoc-display 'simple
treemacs-file-event-delay 2000
treemacs-file-extension-regex treemacs-last-period-regex-value
treemacs-file-follow-delay 0.2
treemacs-file-name-transformer #'identity
treemacs-follow-after-init t
treemacs-expand-after-init t
treemacs-find-workspace-method 'find-for-file-or-pick-first
treemacs-git-command-pipe ""
treemacs-goto-tag-strategy 'refetch-index
treemacs-header-scroll-indicators '(nil . "^^^^^^")
treemacs-hide-dot-git-directory t
treemacs-indentation 2
treemacs-indentation-string " "
treemacs-is-never-other-window nil
treemacs-max-git-entries 5000
treemacs-missing-project-action 'ask
treemacs-move-forward-on-expand nil
treemacs-no-png-images nil
treemacs-no-delete-other-windows t
treemacs-project-follow-cleanup nil
treemacs-persist-file (expand-file-name ".cache/treemacs-persist" user-emacs-directory)
treemacs-position 'left
treemacs-read-string-input 'from-child-frame
treemacs-recenter-distance 0.1
treemacs-recenter-after-file-follow nil
treemacs-recenter-after-tag-follow nil
treemacs-recenter-after-project-jump 'always
treemacs-recenter-after-project-expand 'on-distance
treemacs-litter-directories '("/node_modules" "/.venv" "/.cask")
treemacs-show-cursor nil
treemacs-show-hidden-files t
treemacs-silent-filewatch nil
treemacs-silent-refresh nil
treemacs-sorting 'alphabetic-asc
treemacs-select-when-already-in-treemacs 'move-back
treemacs-space-between-root-nodes t
treemacs-tag-follow-cleanup t
treemacs-tag-follow-delay 1.5
treemacs-text-scale nil
treemacs-user-mode-line-format nil
treemacs-user-header-line-format nil
treemacs-wide-toggle-width 70
treemacs-width 35
treemacs-width-increment 1
treemacs-width-is-initially-locked t
treemacs-workspace-switch-cleanup nil)
;; The default width and height of the icons is 22 pixels. If you are
;; using a Hi-DPI display, uncomment this to double the icon size.
;;(treemacs-resize-icons 44)
(treemacs-follow-mode t)
(treemacs-filewatch-mode t)
(treemacs-fringe-indicator-mode 'always)
(when treemacs-python-executable
(treemacs-git-commit-diff-mode t))
(pcase (cons (not (null (executable-find "git")))
(not (null treemacs-python-executable)))
(`(t . t)
(treemacs-git-mode 'deferred))
(`(t . _)
(treemacs-git-mode 'simple)))
(treemacs-hide-gitignored-files-mode nil))
:bind
(:map global-map
("M-0" . treemacs-select-window)
("C-x t 1" . treemacs-delete-other-windows)
("C-x t t" . treemacs)
("C-x t d" . treemacs-select-directory)
("C-x t B" . treemacs-bookmark)
("C-x t C-t" . treemacs-find-file)
("C-x t M-t" . treemacs-find-tag)))
(use-package treemacs-projectile
:after (treemacs projectile)
:ensure t)
(use-package treemacs-magit
:after (treemacs magit)
:ensure t)
(use-package treemacs-all-the-icons
:config
(treemacs-load-theme "all-the-icons"))
#+end_src
- Highlight indentation
#+begin_src elisp
(use-package highlight-indent-guides
:config
(setf highlight-indent-guides-method 'bitmap)
(add-hook 'prog-mode-hook 'highlight-indent-guides-mode))
#+end_src
- Move text around
#+begin_src elisp
(use-package move-text
:config
(move-text-default-bindings))
#+end_src
** Looks
:PROPERTIES:
:ID: baaa3b17-3676-4759-b2a0-dc792897862b
:END:
#+begin_src elisp
(use-package doom-themes
:config
(setq doom-rouge-brighter-modeline t
doom-rouge-brighter-comments t)
;; (load-theme 'doom-rouge t)
)
(use-package nimbus-theme
:config
(load-theme 'nimbus t))
#+end_src
Modeline
#+begin_src elisp
(use-package doom-modeline
:init
(setq doom-modeline-height 24)
(doom-modeline-mode 1))
#+end_src
Let's also try smooth-scrolling.
#+begin_src elisp
(pixel-scroll-precision-mode t)
#+end_src
** Applications
:PROPERTIES:
:ID: 9061cb70-e3e7-49d5-8fec-476f36ea3d47
:END:
Non crucial things which should be loaded last. If they fail, nothing crucial is
blocked.
- Spell checking
#+begin_src elisp
(with-eval-after-load "ispell"
(setq ispell-program-name "hunspell")
(setq ispell-dictionary "en_US,de_DE")
(ispell-set-spellchecker-params)
(ispell-hunspell-add-multi-dic "en_US,de_DE")
(setq ispell-personal-dictionary "~/.emacs.d/.hunspell_per_dic"))
#+end_src
#+begin_src elisp
(use-package flyspell
:ensure nil
:hook
(text-mode . flyspell-mode)
(prog-mode . flyspell-prog-mode)
:config
(define-key flyspell-mode-map (kbd "C-,") nil)
(define-key flyspell-mode-map (kbd "C-.") nil)
(define-key flyspell-mode-map (kbd "C-;") #'flyspell-correct-wrapper))
(use-package flyspell-correct
:after (flyspell)
:commands (flyspell-correct-at-point
flyspell-correct-wrapper))
#+end_src
- Notes using denotes
#+begin_src elisp
(setq denote-directory (expand-file-name "denotes" org-directory)
denote-date-prompt-use-org-read-date t)
(use-package denote
:ensure (denote :type git
:host github
:repo "protesilaos/denote"
:branch "main")
:config
(add-hook 'dired-mode-hook #'denote-dired-mode))
#+end_src
- Enhance denote a bit, don't know why these aren't a part of denote itself.
#+begin_src elisp
(defun spook--denote-split-org-subtree (&optional prefix)
"Create new Denote note as an Org file using current Org subtree."
(interactive "P")
(let ((text (org-get-entry))
(heading (org-get-heading :no-tags :no-todo :no-priority :no-comment))
(tags (org-get-tags))
(subdir (when prefix (denote-subdirectory-prompt))))
(delete-region (org-entry-beginning-position) (org-entry-end-position))
(denote heading tags 'org subdir)
(insert text)))
#+end_src
- Setup for taking notes for reading/video-watching I do in Firefox.
#+begin_src elisp
(defvar spook-notes-mode-map (make-sparse-keymap))
(define-key spook-notes-mode-map (kbd "C-c i t") #'spook--insert-yt-ts-note)
(define-minor-mode spook-notes-mode
"Minor mode for taking spooky notes.
It is used to set local keybindings depending on the kind of note
being taken."
:keymap spook-notes-mode-map)
(defun spook--get-ff-yt-current-time ()
"Return current time of youtube video running in Firefox's active tab."
(spookfox-eval-js-in-active-tab
(concat
"(function () {"
"try {"
"const player = document.querySelector('.video-stream');"
"return { time: player.currentTime, url: `${window.location.href}&t=${Math.floor(player.currentTime)}` };"
"} catch(e) { return 0; }"
"})()") t))
(defun spook--insert-yt-ts-note (&optional url)
"Insert note for current timestamp for URL in youtube.
Inserted time is an org yt:// link to youtube video at that time."
(interactive)
(let* ((result (spook--get-ff-yt-current-time))
(time (plist-get result :time))
(url (string-replace "https" "yt" (plist-get result :url))))
(insert (concat "- At [[" url "]["
(format-seconds "%m:%s" time)
"]]\n"))))
(defun spook--url-equal-p (url1 url2)
"Return t if URL1 and URL2 have same host, query and path."
(let ((url1 (url-generic-parse-url url1))
(url2 (url-generic-parse-url url2)))
(and (equal (url-host url1)
(url-host url2))
(equal (url-path-and-query url1)
(url-path-and-query url2)))))
(defun spook--find-denote-for-ff-tab (url &optional subdir)
"Find existing denote entry for firefox tab for URL in denote
SUBDIR.
If previously a note for URL was being taken, return that file;
nil otherwise."
(let ((case-fold-search t)
(subdir (expand-file-name subdir denote-directory))
(source-rx (rx "#+source: " (group (+ any) (not "#")))))
(seq-find
(lambda (file)
(with-temp-buffer
(insert-file-contents file)
(search-forward-regexp source-rx nil t)
(spook--url-equal-p url (string-trim (or (match-string 1) "")))))
(mapcar
(lambda (f) (expand-file-name f subdir))
(cl-remove-if-not
(lambda (f) (string-match-p ".org$" f))
(directory-files subdir))))))
(defun spook--denote-ff-tab ()
"Create a new denote for current Firefox tab."
(interactive)
(let* ((tab (spookfox-request-active-tab))
(url (plist-get tab :url))
(yt-p (string-match-p "youtube.com" url))
(tags '("reading"))
(existing-denote (spook--find-denote-for-ff-tab url "reading")))
(if existing-denote
(find-file existing-denote)
(when yt-p
(push "video" tags))
(denote (denote-title-prompt (plist-get tab :title))
tags "org" (expand-file-name "reading" denote-directory))
(when yt-p (spook-notes-mode))
(delete-region (point) (line-beginning-position 0))
(insert (concat "#+source: " url "\n\n")))))
(defun spook--micro-post ()
"Quickly create a micro-post."
(interactive)
(let* ((body (read-from-minibuffer "Micro-Post body: "))
(title (denote-title-prompt (concat (string-trim (substring body 0 (min (length body) 40)))
(when (> (length body) 40) "...")))))
(denote title '("micro" "blog-post"))
(delete-region (point) (line-beginning-position 0))
(insert "#+published-on: ((mastodon . \"\"))\n\n")
(insert body)))
#+end_src
- CRM
#+begin_src elisp
(defvar crm-directory (expand-file-name "crm" denote-directory))
(defun spook-crm--open-or-create ()
"Find or create CRM entry."
(interactive)
(let ((denote-directory crm-directory))
(call-interactively #'denote-open-or-create)))
(defun spook-crm--link-or-create ()
"Find or create CRM entry."
(interactive)
(let ((denote-directory crm-directory))
(call-interactively #'denote-link-or-create)))
#+end_src
- Keyboard shortcuts for fluent note-taking/reading
#+begin_src elisp
(spook--defkeymap
"spook-notes" "C-c n"
'("n" . denote-open-or-create)
'("N" . denote-link-or-create)
'("b" . denote-link-backlinks)
'("d" . spook--diary-today)
'("r" . spook--denote-ff-tab)
'("p" . spook-crm--open-or-create)
'("P" . spook-crm--link-or-create)
'("m" . spook--micro-post))
#+end_src
- Diary
#+begin_src elisp
(defun spook--find-habit (title)
"Find the habit with TITLE in current buffer."
(cl-block 'spook--find-habit
(org-map-entries
(lambda ()
(let ((el (org-element-at-point-no-context)))
(when (and (seq-contains-p (org-get-tags el) "habit" #'equal)
(equal (downcase (org-element-property :raw-value el))
(downcase title)))
(cl-return-from 'spook--find-habit el)))))))
(defun spook--mark-habit-as-done (habit)
"Mark HABIT as done."
(with-current-buffer (find-file-noselect (expand-file-name "TODOs.org" org-directory))
(org-mode)
(let ((el (cl-case habit
(diary (spook--find-habit "write diary entry")))))
(goto-char (org-element-property :begin el))
(org-todo 'done))))
(defun spook--diary-today ()
"Go to today's diary entry."
(interactive)
(let ((denote-directory (expand-file-name "diary" denote-directory))
(title (format-time-string "%Y-%m-%d")))
(if-let ((file (seq-find
(lambda (f) (string-match-p title f))
(directory-files denote-directory))))
(progn
(find-file (expand-file-name file denote-directory))
(goto-char (point-max)))
(spook--mark-habit-as-done 'diary)
(denote title '("diary")))))
#+end_src
- Work notes
#+begin_src elisp
(defun spook--workday-notes (prefix)
"Go to work notes for today plus PREFIX days."
(interactive "P")
(let* ((days (if prefix (prefix-numeric-value prefix) 0))
(denote-directory (expand-file-name "work" denote-directory))
(date (time-add (current-time) (days-to-time days)))
(title (format-time-string "%Y-%m-%d" date)))
(if-let ((file (seq-find
(lambda (f) (string-match-p title f))
(directory-files denote-directory)))
(file (expand-file-name file denote-directory)))
(progn
(find-file file)
;; Remove any other denotes/work file from agenda
;; Assuming that this will always remove older workday files
(setf org-agenda-files
(seq-filter
(lambda (file)
(not (string-match-p "denotes/work" file)))
org-agenda-files))
(org-agenda-file-to-front file)
(goto-char (point-max)))
(denote title '("work") "org" nil title))))
(spook--defkeymap
"workday" "C-c n w"
'("w" . spook--workday-notes)
'("i" . on-issue-note-open-or-create)
'("I" . on-issue-note-link-or-create))
#+end_src
- dirvish for more powerful dired
#+begin_src elisp
(use-package all-the-icons)
(use-package dirvish
:init
(dirvish-override-dired-mode)
:config
(setq dirvish-attributes
'(vc-state subtree-state all-the-icons collapse file-size))
:bind
(("C-c f" . dirvish-fd)
:map dirvish-mode-map
("/" . dirvish-narrow)
("a" . dirvish-quick-access)
("f" . dirvish-file-info-menu)
("y" . dirvish-yank-menu)
("N" . dirvish-narrow)
("^" . dirvish-history-last)
("h" . dirvish-history-jump) ; remapped `describe-mode'
("s" . dirvish-quicksort) ; remapped `dired-sort-toggle-or-edit'
("v" . dirvish-vc-menu) ; remapped `dired-view-file'
("TAB" . dirvish-subtree-toggle)
("M-f" . dirvish-history-go-forward)
("M-b" . dirvish-history-go-backward)
("M-l" . dirvish-ls-switches-menu)
("M-m" . dirvish-mark-menu)
("M-t" . dirvish-layout-toggle)
("M-s" . dirvish-setup-menu)
("M-e" . dirvish-emerge-menu)
("M-j" . dirvish-fd-jump)))
#+end_src
- Ledger
#+begin_src elisp
(use-package ledger-mode
:mode "\\.ledger\\'"
:config
(setq ledger-default-date-format ledger-iso-date-format))
#+end_src
- spookfox
#+begin_src elisp
(when (file-exists-p "~/Documents/work/spookfox")
(use-package spookfox
:ensure (spookfox :type git
:repo "~/Documents/work/spookfox"
:files ("lisp/*.el" "lisp/apps/*.el"))
:config
(setq spookfox-enabled-apps (list spookfox-jscl spookfox-tabs spookfox-js-injection))
(setq spookfox-saved-tabs-target
`(file+headline ,(expand-file-name "spookfox.org" org-directory) "Tabs"))
(spookfox-init))
(defun spook--switch-tab-and-focus ()
"Switch to browser tab and bring browser in focus."
(interactive)
(spookfox-switch-tab)
(when (eq 'darwin system-type)
(ns-do-applescript "tell application \"Firefox\"\n\tactivate\n\tend tell")))
(define-key spook-buffers-keymap (kbd "t") #'spook--switch-tab-and-focus))
#+end_src
- saunf
Use the local repo; very risky, should change.
#+begin_src elisp
(when (file-exists-p (expand-file-name "~/Documents/work/saunf"))
(use-package saunf
:after sly
:ensure (saunf :type git
:repo "~/Documents/work/saunf"
:files ("src/saunf.el"))))
#+end_src
- org-noter
#+begin_src elisp
(use-package nov
:mode ("\\.epub\\'" . nov-mode))
;; (use-package org-noter)
#+end_src
- Shelldon
Let's try replacing alacritty with async-shell-command
#+begin_src elisp
(use-package shelldon
:ensure (shelldon :type git
:host github
:repo "Overdr0ne/shelldon"
:branch "master"
:files ("shelldon.el"))
:config
(setq shell-command-switch "-ic")
(add-hook 'shelldon-mode-hook 'ansi-color-for-comint-mode-on)
(add-to-list 'comint-output-filter-functions 'ansi-color-process-output)
(autoload 'ansi-color-for-comint-mode-on "ansi-color" nil t)
(global-set-key (kbd "M-s") #'shelldon)
(global-set-key (kbd "M-S") #'shelldon-loop))
#+end_src
Shelldon recommends installing bash-complete.
#+begin_src elisp
(use-package bash-completion
:config
(autoload 'bash-completion-dynamic-complete
"bash-completion"
"BASH completion hook")
(add-hook 'shell-dynamic-complete-functions
'bash-completion-dynamic-complete))
#+end_src
- Enable listing shelldon buffers.
Shelldon hides its buffers as soon as output window is hidden. That is fine
for one-off commands, but I also run long-running commands like dev-servers
etc, which need to be closed manually and also need to check the output for
errors.
#+begin_src elisp
(defvar shell-output-history nil)
(defun spook--switch-shell-output ()
"Select shelldon output buffers."
(interactive)
(consult-buffer
(list
`(:name "Shell Output"
:narrow 98
:category buffer
:face consult-buffer
:history shell-output-history
:state consult--buffer-state
:default t
:items
(lambda ()
(consult--buffer-query
:exclude nil
:include "shelldon"
:as #'buffer-name))))))
(define-key spook-buffers-keymap (kbd "o") #'spook--switch-shell-output)
#+end_src
- Quick helper to delete all shelldon buffers, because sometimes there are a lot of them. It is
hard to delete them because they are hidden and don't show up in ibuffer
#+begin_src elisp
(defun spook--delete-all-shelldon-buffers ()
(interactive)
(cl-dolist (buf (cl-remove-if-not
(lambda (buf) (s-contains-p "*shelldon" (buffer-name buf)))
(buffer-list)))
(kill-buffer buf)))
#+end_src
- Irc
Small utility to quickly connect to irc.
#+begin_src elisp
;; (setq
;; erc-nick "bitspook"
;; erc-password (auth-source-pass-get 'secret "libera.chat/bitspook")
;; erc-autojoin-channels-alist '(("libera.chat" "#commonlisp" "#emacs" "#emacs-berlin" "#clschool" "#whereiseveryone" "#lispcafe")))
#+end_src
- Terraform
#+begin_src elisp
(use-package terraform-mode)
#+end_src
- Eww
#+begin_src elisp
(add-hook
'eww-mode-hook
(lambda ()
(display-line-numbers-mode -1)))
#+end_src
- org-download to easily attach images in my notes
#+begin_src elisp
(use-package org-download
:init
(setq-default org-download-method 'attach
org-download-image-dir (expand-file-name "data" org-directory))
:config
;; Drag-and-drop to `dired`
(add-hook 'dired-mode-hook 'org-download-enable))
#+end_src
** Private work related config
:PROPERTIES:
:ID: 6CA859BF-001A-48A8-8FAF-A522EE7FC8B1
:END:
#+begin_src elisp
(elpaca-wait)
(let ((private-config (expand-file-name "./private.el" user-emacs-directory)))
(when (file-exists-p private-config)
(load-file private-config)))
#+end_src