about summary refs log tree commit diff
path: root/users/tazjin/emacs
diff options
context:
space:
mode:
Diffstat (limited to 'users/tazjin/emacs')
-rw-r--r--users/tazjin/emacs/config/bindings.el63
-rw-r--r--users/tazjin/emacs/config/custom.el27
-rw-r--r--users/tazjin/emacs/config/desktop.el331
-rw-r--r--users/tazjin/emacs/config/functions.el248
-rw-r--r--users/tazjin/emacs/config/init.el186
-rw-r--r--users/tazjin/emacs/config/look-and-feel.el102
-rw-r--r--users/tazjin/emacs/config/mail-setup.el16
-rw-r--r--users/tazjin/emacs/config/modes.el37
-rw-r--r--users/tazjin/emacs/config/settings.el44
-rw-r--r--users/tazjin/emacs/default.nix304
10 files changed, 719 insertions, 639 deletions
diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el
index 4e1f341e32..d8b63e33e4 100644
--- a/users/tazjin/emacs/config/bindings.el
+++ b/users/tazjin/emacs/config/bindings.el
@@ -1,11 +1,11 @@
+;; Switch buffers reliably in the face of spurious renames.
+(global-set-key (kbd "C-x b") #'reliably-switch-buffer)
+
 ;; Font size
 (define-key global-map (kbd "C-=") 'increase-default-text-scale) ;; '=' because there lies '+'
 (define-key global-map (kbd "C--") 'decrease-default-text-scale)
 (define-key global-map (kbd "C-x C-0") 'set-default-text-scale)
 
-;; What does <tab> do? Well, it depends ...
-(define-key prog-mode-map (kbd "<tab>") #'company-indent-or-complete-common)
-
 ;; imenu instead of insert-file
 (global-set-key (kbd "C-x i") 'imenu)
 
@@ -15,8 +15,6 @@
 ;; Start eshell or switch to it if it's active.
 (global-set-key (kbd "C-x m") 'eshell)
 
-;; Start a new eshell even if one is active.
-(global-set-key (kbd "C-x C-p") 'ivy-browse-repositories)
 (global-set-key (kbd "M-g M-g") 'goto-line-with-feedback)
 
 ;; Miscellaneous editing commands
@@ -24,7 +22,7 @@
 (global-set-key (kbd "C-c a") 'align-regexp)
 (global-set-key (kbd "C-c m") 'mc/mark-dwim)
 
-;; Browse URLs (very useful for Gitlab's SSH output!)
+;; Browse URLs (very useful for Gerrit's push output, etc!)
 (global-set-key (kbd "C-c b p") 'browse-url-at-point)
 (global-set-key (kbd "C-c b b") 'browse-url)
 
@@ -41,8 +39,11 @@
 ;; Insert TODO comments
 (global-set-key (kbd "C-c t") 'insert-todo-comment)
 
-;; Make sharing music easier
-(global-set-key (kbd "s-s w") #'songwhip-lookup-url)
+;; Open the depot
+(global-set-key (kbd "s-s d") #'tvl-depot-status)
+
+;; Open any project through zoxide
+(global-set-key (kbd "s-s r") #'zoxide-open-project)
 
 ;; Add subthread collapsing to notmuch-show.
 ;;
@@ -54,4 +55,50 @@
     (interactive)
     (notmuch-show-open-or-close-subthread t))) ;; open
 
+;; Get rid of the annoying `save-some-buffers' shortcut which I
+;; *NEVER* use intentionally.
+(unbind-key (kbd "C-x s") 'global-map)
+
+;; German keyboard layout with Y and Z in the correct place.
+
+(quail-define-package
+ "german-qwerty" "German" "DE@" t
+ "German (Deutsch) input method with QWERTY keys"
+ nil t t t t nil nil nil nil nil t)
+
+;; 1!  2"  3§  4$  5%  6&  7/  8(  9)  0=  ß?  [{  ]}
+;;  qQ  wW  eE  rR  tT  yY  uU  iI  oO  pP  üÜ  +*
+;;   aA  sS  dD  fF  gG  hH  jJ  kK  lL  öÖ  äÄ  #^
+;;    zZ  xX  cC  vV  bB  nN  mM  ,;  .:  -_
+
+(quail-define-rules
+ ("-" ?ß)
+ ("=" ?\[)
+ ("`" ?\])
+ ("[" ?ü)
+ ("]" ?+)
+ (";" ?ö)
+ ("'" ?ä)
+ ("\\" ?#)
+ ("/" ?-)
+
+ ("@" ?\")
+ ("#" ?§)
+ ("^" ?&)
+ ("&" ?/)
+ ("*" ?\()
+ ("(" ?\))
+ (")" ?=)
+ ("_" ??)
+ ("+" ?{)
+ ("~" ?})
+ ("{" ?Ü)
+ ("}" ?*)
+ (":" ?Ö)
+ ("\"" ?Ä)
+ ("|" ?^)
+ ("<" ?\;)
+ (">" ?:)
+ ("?" ?_))
+
 (provide 'bindings)
diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el
index a157c7a5fa..3e9a9dcd06 100644
--- a/users/tazjin/emacs/config/custom.el
+++ b/users/tazjin/emacs/config/custom.el
@@ -6,11 +6,7 @@
  '(ac-auto-show-menu 0.8)
  '(ac-delay 0.2)
  '(avy-background t)
- '(cargo-process--custom-path-to-bin "env CARGO_INCREMENTAL=1 cargo")
  '(cargo-process--enable-rust-backtrace 1)
- '(company-auto-complete (quote (quote company-explicit-action-p)))
- '(company-idle-delay 0.5)
- '(custom-enabled-themes (quote (gruber-darker)))
  '(custom-safe-themes
    (quote
     ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default)))
@@ -27,26 +23,3 @@
  '(ns-right-command-modifier (quote meta))
  '(require-final-newline (quote visit-save))
  '(tls-program (quote ("gnutls-cli --x509cafile %t -p %p %h"))))
-(custom-set-faces
- ;; custom-set-faces was added by Custom.
- ;; If you edit it by hand, you could mess it up, so be careful.
- ;; Your init file should contain only one such instance.
- ;; If there is more than one, they won't work right.
- '(default ((t (:foreground "#e4e4ef" :background "#181818"))))
- '(rainbow-delimiters-depth-1-face ((t (:foreground "#2aa198"))))
- '(rainbow-delimiters-depth-2-face ((t (:foreground "#b58900"))))
- '(rainbow-delimiters-depth-3-face ((t (:foreground "#268bd2"))))
- '(rainbow-delimiters-depth-4-face ((t (:foreground "#dc322f"))))
- '(rainbow-delimiters-depth-5-face ((t (:foreground "#859900"))))
- '(rainbow-delimiters-depth-6-face ((t (:foreground "#268bd2"))))
- '(rainbow-delimiters-depth-7-face ((t (:foreground "#cb4b16"))))
- '(rainbow-delimiters-depth-8-face ((t (:foreground "#d33682"))))
- '(rainbow-delimiters-depth-9-face ((t (:foreground "#839496"))))
- '(term-color-black ((t (:background "#282828" :foreground "#282828"))))
- '(term-color-blue ((t (:background "#96a6c8" :foreground "#96a6c8"))))
- '(term-color-cyan ((t (:background "#1fad83" :foreground "#1fad83"))))
- '(term-color-green ((t (:background "#73c936" :foreground "#73c936"))))
- '(term-color-magenta ((t (:background "#9e95c7" :foreground "#9e95c7"))))
- '(term-color-red ((t (:background "#f43841" :foreground "#f43841"))))
- '(term-color-white ((t (:background "#f5f5f5" :foreground "#f5f5f5"))))
- '(term-color-yellow ((t (:background "#ffdd33" :foreground "#ffdd33")))))
diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el
index 38da8f75bc..aa232fec2f 100644
--- a/users/tazjin/emacs/config/desktop.el
+++ b/users/tazjin/emacs/config/desktop.el
@@ -4,13 +4,27 @@
 ;; window-management (EXWM) as well as additional system-wide
 ;; commands.
 
-(require 's)
-(require 'f)
-(require 'dash)
 (require 'exwm)
 (require 'exwm-config)
 (require 'exwm-randr)
 (require 'exwm-systemtray)
+(require 'exwm-xim )
+(require 'f)
+(require 'ring)
+(require 's)
+(require 'seq)
+
+(defcustom tazjin--screen-lock-command "tazjin-screen-lock"
+  "Command to execute for locking the screen."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-increase-command "light -A 4"
+  "Command to increase screen brightness."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-decrease-command "light -U 4"
+  "Command to decrease screen brightness."
+  :group 'tazjin)
 
 (defun pactl (cmd)
   (shell-command (concat "pactl " cmd))
@@ -22,24 +36,14 @@
 
 (defun brightness-up ()
   (interactive)
-  (shell-command "xbacklight -inc 5")
+  (shell-command tazjin--backlight-increase-command)
   (message "Brightness increased"))
 
 (defun brightness-down ()
   (interactive)
-  (shell-command "xbacklight -dec 5")
+  (shell-command tazjin--backlight-decrease-command)
   (message "Brightness decreased"))
 
-(defun lock-screen ()
-  (interactive)
-  ;; A sudoers configuration is in place that lets me execute this
-  ;; particular command without having to enter a password.
-  ;;
-  ;; The reason for things being set up this way is that I want
-  ;; xsecurelock.service to be started as a system-wide service that
-  ;; is tied to suspend.target.
-  (shell-command "/usr/bin/sudo /usr/bin/systemctl start xsecurelock.service"))
-
 (defun set-xkb-layout (layout)
   "Set the current X keyboard layout."
 
@@ -47,6 +51,12 @@
   (shell-command "setxkbmap -option caps:super")
   (message "Set X11 keyboard layout to '%s'" layout))
 
+(defun lock-screen ()
+  (interactive)
+  (set-xkb-layout "us")
+  (deactivate-input-method)
+  (shell-command tazjin--screen-lock-command))
+
 (defun create-window-name ()
   "Construct window names to be used for EXWM buffers by
   inspecting the window's X11 class and title.
@@ -60,39 +70,22 @@
   human-accessible titles."
 
   (pcase (list (or exwm-class-name "unknown") (or exwm-title "unknown"))
-    ;; In Cider windows, rename the class and keep the workspace/file
-    ;; as the title.
-    (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider" title))) title))
-     (format "Cider<%s>" (s-chop-suffix " - Cider" title)))
+    ;; Yandex.Music -> `Я.Music<... stuff ...>'
+    (`("Chromium-browser" ,(and (pred (lambda (title) (s-starts-with? "Yandex.Music - " title))) title))
+     (format "Я.Music<%s>" (s-chop-prefix "Yandex.Music - " title)))
 
-    ;; Attempt to detect IRCCloud windows via their title, which is a
-    ;; combination of the channel name and network.
-    ;;
-    ;; This is what would often be referred to as a "hack". The regexp
-    ;; will not work if a network connection buffer is selected in
-    ;; IRCCloud, but since the title contains no other indication that
-    ;; we're dealing with an IRCCloud window
-    (`("Google-chrome"
-       ,(and (pred (lambda (title)
-                     (s-matches? "^[\*\+]\s#[a-zA-Z0-9/\-]+\s\|\s[a-zA-Z\.]+$" title)))
-             title))
-     (format "IRCCloud<%s>" title))
-
-    ;; For other Chrome windows, make the title shorter.
-    (`("Google-chrome" ,title)
-     (format "Chrome<%s>" (s-truncate 42 (s-chop-suffix " - Google Chrome" title))))
-
-    ;; Gnome-terminal -> Term
-    (`("Gnome-terminal" ,title)
-     ;; fish-shell buffers contain some unnecessary whitespace and
-     ;; such before the current working directory. This can be
-     ;; stripped since most of my terminals are fish shells anyways.
-     (format "Term<%s>" (s-trim-left (s-chop-prefix "fish" title))))
+    ;; For other Chromium windows, make the title shorter.
+    (`("Chromium-browser" ,title)
+     (format "Chromium<%s>" (s-truncate 42 (s-chop-suffix " - Chromium" title))))
+
+    ;; similarly for Firefox
+    (`("firefox" ,title)
+     (format "FF<%s>" title))
 
     ;; Quassel buffers
     ;;
     ;; These have a title format that looks like:
-    ;; "Quassel IRC - ##tvl (Freenode) — Quassel IRC"
+    ;; "Quassel IRC - #tvl (hackint) — Quassel IRC"
     (`("quassel" ,title)
      (progn
        (if (string-match
@@ -109,9 +102,6 @@
     (`(,class ,title) (format "%s<%s>" class (s-truncate 12 title)))))
 
 ;; EXWM launch configuration
-;;
-;; This used to use use-package, but when something breaks use-package
-;; it doesn't exactly make debugging any easier.
 
 (let ((titlef (lambda ()
                 (exwm-workspace-rename-buffer (create-window-name)))))
@@ -119,23 +109,57 @@
   (add-hook 'exwm-update-title-hook titlef))
 
 (fringe-mode 3)
-(exwm-enable)
 
-;; 's-N': Switch to certain workspace
-(setq exwm-workspace-number 10)
-(dotimes (i 10)
-  (exwm-input-set-key (kbd (format "s-%d" i))
-                      `(lambda ()
-                         (interactive)
-                         (exwm-workspace-switch-create ,i))))
+;; tab-bar related config
+(setq tab-bar-show 1)
+(setq tab-bar-tab-hints t)
 
-;; Launch applications / any command  with completion (dmenu style!)
-(exwm-input-set-key (kbd "s-d") #'counsel-linux-app)
-(exwm-input-set-key (kbd "s-x") #'ivy-run-external-command)
-(exwm-input-set-key (kbd "s-p") #'ivy-password-store)
+(setq tab-bar-format
+      '(tab-bar-format-history
+        tab-bar-format-tabs tab-bar-separator
+        tab-bar-format-align-right tab-bar-format-global))
 
-;; Add X11 terminal selector to a key
-(exwm-input-set-key (kbd "C-x t") #'ts/switch-to-terminal)
+(setq tab-bar-new-tab-choice
+      (lambda () (get-buffer-create "*scratch*")))
+
+(tab-bar-mode 1)
+
+(setq x-no-window-manager t) ;; TODO(tazjin): figure out when to remove this
+(exwm-enable)
+(exwm-randr-enable)
+
+;; Tab-management shortcuts
+
+(defun tab-bar-select-or-return ()
+  "This function behaves like `tab-bar-select-tab', except it calls
+`tab-recent' if asked to jump to the current tab. This simulates
+the back&forth behaviour of i3."
+  (interactive)
+  (let* ((key (event-basic-type last-command-event))
+         (tab (if (and (characterp key) (>= key ?1) (<= key ?9))
+                  (- key ?0)
+                0))
+         (current (1+ (tab-bar--current-tab-index))))
+    (if (eq tab current)
+        (tab-recent)
+      (tab-bar-select-tab tab))))
+
+(dotimes (i 8)
+  (exwm-input-set-key (kbd (format "s-%d" (+ 1 i))) #'tab-bar-select-or-return))
+
+(exwm-input-set-key (kbd "s-9") #'tab-last)
+(exwm-input-set-key (kbd "s-f") #'tab-next)
+(exwm-input-set-key (kbd "s-b") #'tab-recent)
+(exwm-input-set-key (kbd "s-w") #'tab-close)
+(exwm-input-set-key (kbd "s-n") #'tab-new)
+
+;; Launch applications / any command with completion (dmenu style!)
+(exwm-input-set-key (kbd "s-d") #'run-xdg-app)
+(exwm-input-set-key (kbd "s-x") #'run-external-command)
+(exwm-input-set-key (kbd "s-p") #'password-store-lookup)
+
+;; Add vterm selector to a key
+(exwm-input-set-key (kbd "s-v") #'ts/switch-to-terminal)
 
 ;; Toggle between line-mode / char-mode
 (exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard)
@@ -162,19 +186,19 @@
 (bind-xkb "no" "k n")
 (bind-xkb "ru" "k r")
 (bind-xkb "se" "k s")
+(bind-xkb "us" "л г")
+(bind-xkb "de" "л в")
+(bind-xkb "no" "л т")
+(bind-xkb "ru" "л к")
 
-;; These are commented out because Emacs no longer starts (??) if
-;; they're set at launch.
-;;
-;; (bind-xkb "us" "л г")
-;; (bind-xkb "de" "л в")
-;; (bind-xkb "no" "л т")
-;; (bind-xkb "ru" "л к")
+;; Configuration of EXWM input method handling for X applications
+(exwm-xim-enable)
+(setq default-input-method "russian-computer")
+(push ?\C-\\ exwm-input-prefix-keys)
 
 ;; Line-editing shortcuts
-(exwm-input-set-simulation-keys
- '(([?\C-d] . delete)
-   ([?\C-w] . ?\C-c)))
+(exwm-input-set-simulation-key (kbd "C-d") (kbd "DEL"))
+(exwm-input-set-simulation-key (kbd "C-w") (kbd "C-c"))
 
 ;; Show time & battery status in the mode line
 (display-time-mode)
@@ -183,80 +207,123 @@
 ;; enable display of X11 system tray within Emacs
 (exwm-systemtray-enable)
 
-;; Configure xrandr (multi-monitor setup).
+;; Multi-monitor configuration.
 ;;
-;; This makes some assumptions about how my machines are connected to
-;; my home setup during the COVID19 isolation period.
-
-(defun set-randr-config (screens)
-  (setq exwm-randr-workspace-monitor-plist
-        (-flatten (-map (lambda (screen)
-                          (-map (lambda (screen-id) (list screen-id (car screen))) (cdr screen)))
-                        screens))))
-
-;; Layouts for Vauxhall (laptop)
+;; With tab-bar-mode, each monitor only displays at most one
+;; workspace. Workspaces are only created, never deleted, meaning that
+;; the number of workspaces will be equivalent to the maximum number
+;; of displays that were connected during a session.
+;;
+;; The first workspace is special: It is kept on the primary monitor.
 
-(defun randr-vauxhall-layout-single ()
-  "Laptop screen only!"
+(defun exwm-assign-workspaces ()
+  "Assigns workspaces to the currently existing monitors, putting
+the first one on the primary display and allocating the others
+dynamically if needed in no particular order."
   (interactive)
-  (set-randr-config '(("eDP1" (number-sequence 0 9))))
-  (shell-command "xrandr --output eDP1 --auto --primary")
-  (shell-command "xrandr --output HDMI1 --off")
-  (shell-command "xrandr --output DP2 --off")
-  (exwm-randr-refresh))
-
-(defun randr-vauxhall-layout-all ()
-  "Use all screens at home."
+  (let* ((randr-monitors (exwm-randr--get-monitors))
+         (primary (car randr-monitors))
+         (all-monitors (seq-map #'car (cadr randr-monitors)))
+         (sorted-primary-first (seq-sort (lambda (a b)
+                                           (or (equal a primary)
+                                               (< a b)))
+                                         all-monitors))
+         ;; assign workspace numbers to each monitor ...
+         (workspace-assignments
+          (flatten-list (seq-map-indexed (lambda (monitor idx)
+                                           (list idx monitor))
+                                         sorted-primary-first))))
+    ;; ensure that the required workspaces exist
+    (exwm-workspace-switch-create (- (seq-length all-monitors) 1))
+
+    ;; update randr config
+    (setq exwm-randr-workspace-monitor-plist workspace-assignments)
+    (exwm-randr-refresh)
+
+    ;; leave focus on primary workspace
+    (exwm-workspace-switch 0)))
+
+(defun list-available-monitors ()
+  "List connected, but unused monitors."
+  (let* ((all-connected
+          (seq-map (lambda (line) (car (s-split " " line)))
+                   (s-lines (s-trim (shell-command-to-string "xrandr | grep connected | grep -v disconnected")))))
+         (all-active (seq-map #'car (cadr (exwm-randr--get-monitors)))))
+    (seq-filter (lambda (s) (not (seq-contains-p all-active s)))
+                all-connected)))
+
+(defun exwm-enable-monitor ()
+  "Interactively construct an EXWM invocation that enable the
+given monitor and assigns a workspace to it."
   (interactive)
-  (set-randr-config
-   '(("eDP1" 0)
-     ("HDMI1" 1 2 3 4 5)
-     ("DP2" 6 7 8 9)))
-
-  (shell-command "xrandr --output HDMI1 --right-of eDP1 --auto --primary")
-  (shell-command "xrandr --output DP2 --right-of HDMI1 --auto --rotate left")
-  (exwm-randr-refresh))
 
-(defun randr-vauxhall-layout-wide-only ()
-  "Use only the wide screen at home."
+  (let* ((monitors (list-available-monitors))
+         (primary (car (exwm-randr--get-monitors)))
+         (monitor (pcase (seq-length monitors)
+                    (0 (error "No available monitors."))
+                    (1 (car monitors))
+                    (_
+                     (completing-read "Which monitor? " (list-available-monitors) nil t))))
+
+         (configurations `(("secondary (left)" . ,(format "--left-of %s" primary))
+                           ("secondary (right)" . ,(format "--right-of %s" primary))
+                           ("primary (left)" . ,(format "--left-of %s --primary" primary))
+                           ("primary (right)" . ,(format "--right-of %s --primary" primary))
+                           ("mirror" . ,(format "--same-as %s" primary))))
+
+         (where (completing-read (format "%s should be " monitor)
+                                 (seq-map #'car configurations)
+                                 nil t))
+         (xrandr-pos (cdr (assoc where configurations)))
+         (xrandr-cmd (format "xrandr --output %s --auto %s" monitor xrandr-pos)))
+    (message "Invoking '%s'" xrandr-cmd)
+    (shell-command xrandr-cmd)
+    (exwm-assign-workspaces)))
+
+(defun exwm-disable-monitor ()
+  "Interactively choose a monitor to disable."
   (interactive)
-  (set-randr-config
-   '(("eDP1" 8 9 0)
-     ("HDMI1" 1 2 4 5 6 7)))
 
-  (shell-command "xrandr --output DP2 --off")
-  (shell-command "xrandr --output HDMI1 --right-of eDP1 --auto --primary")
-  (exwm-randr-refresh))
+  (let* ((all (exwm-randr--get-monitors))
+         (active (seq-map #'car (cadr all)))
+         (monitor (if (> (seq-length active) 1)
+                      (completing-read "Disable which monitor? " active nil t)
+                    (error "Only one monitor is active!")))
 
-;; Layouts for frog (desktop)
+         ;; If this monitor was primary, pick another active one instead.
+         (remaining (seq-filter (lambda (s) (not (equal s monitor))) active))
+         (new-primary
+          (when (equal monitor (car all))
+            (pcase (seq-length remaining)
+              (1 (car remaining))
+              (_ (completing-read "New primary? " remaining nil t))))))
 
-(defun randr-frog-layout-right-only ()
-  "Use only the right screen on frog."
-  (interactive)
-  (set-randr-config `(("DisplayPort-0" ,(number-sequence 0 9))))
-  (shell-command "xrandr --output DisplayPort-0 --off")
-  (shell-command "xrandr --output DisplayPort-1 --auto --primary"))
+    (when new-primary
+      (shell-command (format "xrandr --output %s --primary" new-primary)))
 
-(defun randr-frog-layout-both ()
-  "Use the left and right screen on frog."
-  (interactive)
-  (set-randr-config `(("DisplayPort-0" 1 2 3 4 5)
-                      ("DisplayPort-1" 6 7 8 9 0)))
+    (shell-command (format "xrandr --output %s --off" monitor))
+    (exwm-assign-workspaces)))
 
-  (shell-command "xrandr --output DisplayPort-0 --auto --primary --left-of DisplayPort-1")
-  (shell-command "xrandr --output DisplayPort-1 --auto --right-of DisplayPort-0 --rotate left"))
-
-(pcase (s-trim (shell-command-to-string "hostname"))
-  ("vauxhall"
-   (exwm-input-set-key (kbd "s-m s") #'randr-vauxhall-layout-single)
-   (exwm-input-set-key (kbd "s-m a") #'randr-vauxhall-layout-all)
-   (exwm-input-set-key (kbd "s-m w") #'randr-vauxhall-layout-wide-only))
-
-  ("frog"
-   (exwm-input-set-key (kbd "s-m b") #'randr-frog-layout-both)
-   (exwm-input-set-key (kbd "s-m r") #'randr-frog-layout-right-only)))
+(defun exwm-switch-monitor ()
+  "Switch focus to another monitor by name."
+  (interactive)
 
-(exwm-randr-enable)
+  ;; TODO: Filter out currently active? How to determine it?
+  (let* ((target (completing-read "Switch to monitor: "
+                                  (seq-map #'car (cadr (exwm-randr--get-monitors)))
+                                  nil t))
+         (target-workspace
+          (cl-loop for (workspace screen) on exwm-randr-workspace-monitor-plist by #'cddr
+                   when (equal screen target) return workspace)))
+    (exwm-workspace-switch target-workspace)))
+
+(exwm-input-set-key (kbd "s-m e") #'exwm-enable-monitor)
+(exwm-input-set-key (kbd "s-m d") #'exwm-disable-monitor)
+(exwm-input-set-key (kbd "s-m o") #'exwm-switch-monitor)
+
+;; Notmuch shortcuts as EXWM globals
+;; (g m => gmail)
+(exwm-input-set-key (kbd "s-g m") #'notmuch)
 
 ;; Let buffers move seamlessly between workspaces by making them
 ;; accessible in selectors on all frames.
diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el
index 9bb6772a27..68a384d20f 100644
--- a/users/tazjin/emacs/config/functions.el
+++ b/users/tazjin/emacs/config/functions.el
@@ -2,9 +2,7 @@
 (require 'dash)
 (require 'map)
 
-(defun load-file-if-exists (filename)
-  (if (file-exists-p filename)
-      (load filename)))
+(require 'gio-list-apps) ;; native module!
 
 (defun goto-line-with-feedback ()
   "Show line numbers temporarily, while prompting for the line number input"
@@ -17,29 +15,19 @@
           (goto-line target)))
     (setq-local display-line-numbers nil)))
 
-;; These come from the emacs starter kit
-
 (defun esk-add-watchwords ()
   (font-lock-add-keywords
    nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)"
           1 font-lock-warning-face t))))
 
+(add-hook 'prog-mode-hook 'esk-add-watchwords)
+
 (defun esk-sudo-edit (&optional arg)
   (interactive "p")
   (if (or arg (not buffer-file-name))
       (find-file (concat "/sudo:root@localhost:" (read-file-name "File: ")))
     (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name))))
 
-;; Open Fefes blog
-(defun fefes-blog ()
-  (interactive)
-  (eww "https://blog.fefe.de/"))
-
-;; Open the NixOS man page
-(defun nixos-man ()
-  (interactive)
-  (man "configuration.nix"))
-
 ;; Get the nix store path for a given derivation.
 ;; If the derivation has not been built before, this will trigger a build.
 (defun nix-store-path (derivation)
@@ -59,9 +47,6 @@
            (buffer-name)
            require-final-newline))
 
-;; Helm includes a command to run external applications, which does
-;; not seem to exist in ivy. This implementation uses some of the
-;; logic from Helm to provide similar functionality using ivy.
 (defun list-external-commands ()
   "Creates a list of all external commands available on $PATH
   while filtering NixOS wrappers."
@@ -82,9 +67,9 @@
   '(("google-chrome" . "--force-device-scale-factor=1.4"))
 
   "This setting lets me add additional flags to specific commands
-  that are run interactively via `ivy-run-external-command'.")
+  that are run interactively via `run-external-command'.")
 
-(defun run-external-command (cmd)
+(defun run-external-command--handler (cmd)
   "Execute the specified command and notify the user when it
   finishes."
     (let* ((extra-flags (cdr (assoc cmd external-command-flag-overrides)))
@@ -96,59 +81,46 @@
          (when (string= event "finished\n")
            (message "%s process finished." process))))))
 
-(defun ivy-run-external-command ()
+(defun run-external-command ()
   "Prompts the user with a list of all installed applications and
   lets them select one to launch."
 
   (interactive)
   (let ((external-commands-list (list-external-commands)))
-    (ivy-read "Command:" external-commands-list
-              :require-match t
-              :history 'external-commands-history
-              :action #'run-external-command)))
-
-(defun ivy-password-store (&optional password-store-dir)
-  "Custom version of password-store integration with ivy that
-  actually uses the GPG agent correctly."
+    (run-external-command--handler
+     (completing-read "Command: " external-commands-list
+                      nil                             ;; predicate
+                      t                               ;; require-match
+                      nil                             ;; initial-input
+                      ;; hist
+                      'external-commands-history))))
+
+(defun password-store-lookup (&optional password-store-dir)
+  "Interactive password-store lookup function that actually uses
+the GPG agent correctly."
 
   (interactive)
-  (ivy-read "Copy password of entry: "
-            (password-store-list (or password-store-dir (password-store-dir)))
-            :require-match t
-            :keymap ivy-pass-map
-            :action (lambda (entry)
-                      (let ((password (auth-source-pass-get 'secret entry)))
-                        (password-store-clear)
-                        (kill-new password)
-                        (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
-                        (message "Copied %s to the kill ring. Will clear in %s seconds."
-                                 entry (password-store-timeout))
-                        (setq password-store-timeout-timer
-                              (run-at-time (password-store-timeout)
-                                           nil 'password-store-clear))))))
-
-(defun ivy-browse-repositories ()
-  "Select a git repository and open its associated magit buffer."
 
-  (interactive)
-  (ivy-read "Repository: "
-            (magit-list-repos)
-            :require-match t
-            :sort t
-            :action #'magit-status))
-
-(defun bottom-right-window-p ()
-  "Determines whether the last (i.e. bottom-right) window of the
-  active frame is showing the buffer in which this function is
-  executed."
-  (let* ((frame (selected-frame))
-         (right-windows (window-at-side-list frame 'right))
-         (bottom-windows (window-at-side-list frame 'bottom))
-         (last-window (car (seq-intersection right-windows bottom-windows))))
-    (eq (current-buffer) (window-buffer last-window))))
+  (let* ((entry (completing-read "Copy password of entry: "
+                   (password-store-list (or password-store-dir
+                                            (password-store-dir)))
+                   nil ;; predicate
+                   t   ;; require-match
+                   ))
+         (password (or (let ((epa-suppress-error-buffer t))
+                         (auth-source-pass-get 'secret entry))
+                       (error "failed to decrypt '%s', wrong password?" entry))))
+    (password-store-clear)
+    (kill-new password)
+    (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
+    (message "Copied %s to the kill ring. Will clear in %s seconds."
+             entry (password-store-timeout))
+    (setq password-store-timeout-timer
+          (run-at-time (password-store-timeout)
+                       nil 'password-store-clear))))
 
 (defhydra mc/mark-more-hydra (:color pink)
-  ("<up>" mmlte--up "Mark previous like this")
+  ("<up>" mc/mmlte--up "Mark previous like this")
   ("<down>" mc/mmlte--down "Mark next like this")
   ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up)
                                "Skip past the cursor furthest up"
@@ -174,26 +146,15 @@
       (mc/mmlte--down)
       (mc/mark-more-hydra/body))))
 
-(defun memespace-region ()
-  "Make a meme out of it."
+(setq mc/cmds-to-run-for-all '(kill-region paredit-newline))
 
-  (interactive)
-  (let* ((start (region-beginning))
-         (end (region-end))
-         (memed
-          (message
-           (s-trim-right
-            (apply #'string
-                   (-flatten
-                    (nreverse
-                     (-reduce-from (lambda (acc x)
-                                     (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc))
-                                   '()
-                                   (string-to-list (buffer-substring-no-properties start end))))))))))
-
-    (save-excursion (delete-region start end)
-                    (goto-char start)
-                    (insert memed))))
+(setq mc/cmds-to-run-once '(mc/mark-dwim
+                            mc/mark-more-hydra/mc/mmlte--down
+                            mc/mark-more-hydra/mc/mmlte--left
+                            mc/mark-more-hydra/mc/mmlte--right
+                            mc/mark-more-hydra/mc/mmlte--up
+                            mc/mark-more-hydra/mmlte--up
+                            mc/mark-more-hydra/nil))
 
 (defun insert-todo-comment (prefix todo)
   "Insert a comment at point with something for me to do."
@@ -237,11 +198,16 @@
   (if prefix (text-scale-adjust 0)
     (set-face-attribute 'default nil :height (or to 120))))
 
-(defun scrot-select ()
+(defun screenshot-select (filename)
   "Take a screenshot based on a mouse-selection and save it to
   ~/screenshots."
-  (interactive)
-  (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'"))
+  (interactive "sScreenshot filename: ")
+  (let* ((path (f-join "~/screenshots"
+                       (format "%s-%d.png"
+                               (if (string-empty-p filename) "shot" filename)
+                               (time-convert nil 'integer)))))
+    (shell-command (format "maim --select %s" path))
+    (message "Wrote screenshot to %s" path)))
 
 (defun graph-unread-mails ()
   "Create a bar chart of unread mails based on notmuch tags.
@@ -289,6 +255,17 @@
 
 (add-to-list 'project-find-functions #'find-depot-project)
 
+(defun find-cargo-project (dir)
+  "Attempt to find the current project in `project-find-functions'
+by looking for a `Cargo.toml' file."
+  (when dir
+    (unless (equal "/" dir)
+      (if (f-exists-p (f-join dir "Cargo.toml"))
+          (cons 'transient dir)
+        (find-cargo-project (f-parent dir))))))
+
+(add-to-list 'project-find-functions #'find-cargo-project)
+
 (defun magit-find-file-worktree ()
   (interactive)
   "Find a file in the current (ma)git worktree."
@@ -296,31 +273,80 @@
                              (magit-read-file-from-rev "HEAD" "Find file")
                              #'pop-to-buffer-same-window))
 
-(defun songwhip--handle-result (status &optional cbargs)
-  ;; TODO(tazjin): Inspect status, which looks different in practice
-  ;; than the manual claims.
-  (if-let* ((response (json-parse-string
-                       (buffer-substring url-http-end-of-headers (point-max))))
-            (sw-path (ht-get* response "data" "path"))
-            (link (format "https://songwhip.com/%s" sw-path))
-            (select-enable-clipboard t))
-      (progn
-        (kill-new link)
-        (message "Copied Songwhip link (%s)" link))
-    (warn "Something went wrong while retrieving Songwhip link!")
-    ;; For debug purposes, the buffer is persisted in this case.
-    (setq songwhip--debug-buffer (current-buffer))))
-
-(defun songwhip-lookup-url (url)
-  "Look up URL on Songwhip and copy the resulting link to the clipboard."
-  (interactive "sEnter source URL: ")
-  (let ((songwhip-url "https://songwhip.com/api/")
-        (url-request-method "POST")
-        (url-request-extra-headers '(("Content-Type" . "application/json")))
-        (url-request-data
-         (json-serialize `((country . "GB")
-                           (url . ,url)))))
-    (url-retrieve "https://songwhip.com/api/" #'songwhip--handle-result nil t t)
-    (message "Requesting Songwhip URL ... please hold the line.")))
+(defun zoxide-open-project ()
+  "Query Zoxide for paths, and open the result as appropriate (magit or dired)."
+  (interactive)
+  (zoxide-open-with
+   nil
+   (lambda (path)
+     (condition-case err (magit-status-setup-buffer path)
+       (magit-outside-git-repo (dired path))))))
+
+(defun toggle-nix-test-and-exp ()
+  "Switch between the .nix and .exp file in a Tvix/Nix test."
+  (interactive)
+  (let* ((file (buffer-file-name))
+         (other (if (s-suffix? ".nix" file)
+                    (s-replace-regexp ".nix$" ".exp" file)
+                  (if (s-suffix? ".exp" file)
+                      (s-replace-regexp ".exp$" ".nix" file)
+                    (error "Not a .nix/.exp file!")))))
+    (find-file other)))
+
+(defun reliably-switch-buffer ()
+  "Reliably and interactively switch buffers, without ending up in a
+situation where the buffer was renamed during selection and an
+empty new buffer is created.
+
+This is done by, in contrast to most buffer-switching functions,
+retaining a list of the buffer *objects* and their associated
+names, instead of only their names (which might change)."
+
+  (interactive)
+  (let* ((buffers (seq-map (lambda (b) (cons (buffer-name b) b))
+                           (seq-filter (lambda (b) (not (string-prefix-p " " (buffer-name b))))
+                                       (buffer-list))))
+
+         ;; Annotate buffers that display remote files. I frequently
+         ;; want to see it, because I might have identically named
+         ;; files open locally and remotely at the same time, and it
+         ;; helps with differentiating them.
+         (completion-extra-properties
+          '(:annotation-function
+            (lambda (name)
+              (if-let* ((file (buffer-file-name (cdr (assoc name buffers))))
+                        (remote (file-remote-p file)))
+                  (format " [%s]" remote)))))
+
+         (name (completing-read "Switch to buffer: " (seq-map #'car buffers)))
+         (selected (or (cdr (assoc name buffers))
+                       ;; Allow users to manually select invisible buffers ...
+                       (get-buffer name))))
+    (switch-to-buffer (or selected name) nil 't)))
+
+(defun run-xdg-app ()
+  "Use `//users/tazjin/gio-list-apps' to retrieve a list of
+installed (and visible) XDG apps, and let users launch them."
+  (interactive)
+  (let* ((apps (taz-list-xdg-apps))
+
+         ;; Display the command that will be run as an annotation
+         (completion-extra-properties
+          '(:annotation-function (lambda (app) (format " [%s]" (cdr (assoc app apps)))))))
+
+    (run-external-command--handler (cdr (assoc (completing-read "App: " apps nil t) apps)))))
+
+(defun advice-remove-all (sym)
+  "Remove all advices from symbol SYM."
+  (interactive "aFunction symbol: ")
+  (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))
+
+(defun M-x-always-same-window ()
+  "Run `execute-extended-command', but ensure that whatever it does
+always opens in the same window in which the command was invoked."
+  (interactive)
+  (let ((display-buffer-overriding-action
+         '((display-buffer-same-window) . ((inhibit-same-window . nil)))))
+    (call-interactively #'execute-extended-command)))
 
 (provide 'functions)
diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el
index 9f34c60a8d..ced3bf2ff8 100644
--- a/users/tazjin/emacs/config/init.el
+++ b/users/tazjin/emacs/config/init.el
@@ -1,29 +1,18 @@
 ;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*-
 
+;; Disable annoying warnings from native compilation.
+(setq native-comp-async-report-warnings-errors nil
+      warning-suppress-log-types '((comp)))
+
 ;; Packages are installed via Nix configuration, this file only
 ;; initialises the newly loaded packages.
 
 (require 'use-package)
 (require 'seq)
 
-
-;; TODO(tazjin): Figure out what's up with vc.
-;;
-;; Leaving vc enabled breaks all find-file operations with messages
-;; about .git folders being absent, but in random places.
-(require 'vc)
-(setq vc-handled-backends nil)
-
 (package-initialize)
 
 ;; Initialise all packages installed via Nix.
-;;
-;; TODO: Generate this section in Nix for all packages that do not
-;; require special configuration.
-
-;;
-;; Packages providing generic functionality.
-;;
 
 (use-package ace-window
   :bind (("C-x o" . ace-window))
@@ -40,23 +29,12 @@
 
 (use-package browse-kill-ring)
 
-(use-package company
-  :hook ((prog-mode . company-mode))
-  :config (setq company-tooltip-align-annotations t))
-
-(use-package counsel
-  :after (ivy)
-  :config (counsel-mode 1)
-  :bind (("C-c r g" . counsel-rg)))
+(use-package consult
+  :bind
+  ("C-c r g" . consult-ripgrep)
+  ("C-s" . consult-line))
 
 (use-package dash)
-(use-package dash-functional)
-
-(use-package dottime
-  :demand
-  :after (notmuch telega)
-  :config (dottime-display-mode t))
-
 (use-package gruber-darker-theme)
 
 (use-package eglot
@@ -69,32 +47,13 @@
 (use-package hydra)
 (use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode)))
 
-(use-package ivy
-  :config
-  (ivy-mode 1)
-  (setq enable-recursive-minibuffers t)
-  (setq ivy-use-virtual-buffers t))
-
-(use-package ivy-pass :after (ivy))
-
-(use-package ivy-prescient
-  :after (ivy prescient)
-  :config
-  (ivy-prescient-mode)
-  ;; Fixes an issue with how regexes are passed to ripgrep from counsel,
-  ;; see raxod502/prescient.el#43
-  (setf (alist-get 'counsel-rg ivy-re-builders-alist) #'ivy--regex-plus))
-
 (use-package multiple-cursors)
 
 (use-package notmuch
-  :bind (:map global-map
-              ("s-g m" . notmuch)
-              ("s-g M" . counsel-notmuch)) ;; g m -> gmail
-  :config
-  (setq notmuch-search-oldest-first nil)
-  (setq notmuch-show-all-tags-list t)
-  (setq notmuch-hello-tag-list-make-query "tag:unread"))
+  :custom
+  (notmuch-search-oldest-first nil)
+  (notmuch-show-all-tags-list t)
+  (notmuch-hello-tag-list-make-query "tag:unread"))
 
 (use-package paredit :hook ((lisp-mode . paredit-mode)
                             (emacs-lisp-mode . paredit-mode)))
@@ -105,21 +64,20 @@
   (pinentry-start))
 
 (use-package prescient
-  :after (ivy counsel)
-  :config (prescient-persist-mode))
+  :config
+  (prescient-persist-mode)
+  (setq completion-styles '(basic prescient)))
 
 (use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode))
 (use-package rainbow-mode)
 (use-package s)
-(use-package string-edit)
+(use-package string-edit-at-point)
+(use-package term-switcher)
 
-(use-package swiper
-  :after (counsel ivy)
-  :bind (("C-s" . swiper)))
+(use-package undo-tree
+  :config (global-undo-tree-mode)
+  :custom (undo-tree-auto-save-history nil))
 
-(use-package telephone-line) ;; configuration happens outside of use-package
-(use-package term-switcher)
-(use-package undo-tree :config (global-undo-tree-mode))
 (use-package uuidgen)
 (use-package which-key :config (which-key-mode t))
 
@@ -132,41 +90,13 @@
   :config (setq magit-repository-directories '(("/home/tazjin/projects" . 2)
                                                ("/home/tazjin" . 1))))
 
-(use-package org-journal
-  ;; Always use my own key to encrypt files. There seems to be no
-  ;; global way to set this, as `epa-file-encrypt-to' only has an
-  ;; effect as a file-local variable (?!)
-  :hook ((org-journal-mode . (lambda ()
-                               (setq-local epa-file-encrypt-to
-                                           "DCF34CFAC1AC44B87E26333136EE34814F6D294A"))))
-
-  :config
-  (setq org-journal-dir "/ssh:camden.tazj.in:/home/tazjin/journal"
-        org-journal-encrypt-journal t
-        org-journal-file-type 'weekly
-        org-journal-date-format "%A, %Y-%m-%d"
-        org-journal-file-format "%Y%m%d-weekly"
-
-        ;; Saturday, because reasons.
-        org-journal-start-on-weekday 6)
-
-  ;; org-journal doesn't actually enter its mode automatically if
-  ;; encryption is used (I'm not sure why), so this teaches Emacs to
-  ;; recognise the files.
-  (add-to-list 'auto-mode-alist '("[0-9]-weekly\\.gpg\\'" . org-journal-mode)))
-
-(use-package org-ql)
-
 (use-package password-store)
-(use-package pg)
 (use-package restclient)
 
 (use-package vterm
-  :config (progn
-            (setq vterm-shell "fish")
-            (setq vterm-exit-functions
-                  (lambda (&rest _) (kill-buffer (current-buffer))))
-            (setq vterm-kill-buffer-on-exit t)))
+  :custom
+  (vterm-shell "fish")
+  (vterm-kill-buffer-on-exit t))
 
 ;; vterm removed the ability to set a custom title generator function
 ;; via the public API, so this overrides its private title generation
@@ -185,9 +115,9 @@
 (use-package cargo
   :hook ((rust-mode . cargo-minor-mode)
          (cargo-process-mode . visual-line-mode))
-  :bind (:map cargo-minor-mode-map ("C-c C-c C-l" . ignore)))
+  :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore)))
 
-(use-package dockerfile-mode)
+(use-package dockerfile-ts-mode)
 
 (use-package erlang
   :hook ((erlang-mode . (lambda ()
@@ -195,7 +125,6 @@
                           (local-set-key ">" 'self-insert-command)))))
 
 (use-package f)
-(use-package geiser)
 
 (use-package go-mode
   :bind (:map go-mode-map ("C-c C-r" . recompile))
@@ -208,9 +137,7 @@
 
 (use-package ielm
   :hook ((inferior-emacs-lisp-mode . (lambda ()
-                                       (paredit-mode)
-                                       (rainbow-delimiters-mode-enable)
-                                       (company-mode)))))
+                                       (rainbow-delimiters-mode-enable)))))
 
 (use-package jq-mode
   :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)))
@@ -219,11 +146,8 @@
   :hook ((kotlin-mode . (lambda ()
                           (setq indent-line-function #'indent-relative)))))
 
-(use-package lsp-mode)
-
 (use-package markdown-mode
   :config
-  (add-to-list 'auto-mode-alist '("\\.txt\\'" . markdown-mode))
   (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode))
   (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode)))
 
@@ -240,24 +164,58 @@
 (use-package sly
   :hook ((sly-mrepl-mode . (lambda ()
                              (paredit-mode)
-                             (rainbow-delimiters-mode-enable)
-                             (company-mode))))
+                             (rainbow-delimiters-mode-enable))))
   :config
   (setq common-lisp-hyperspec-root "file:///home/tazjin/docs/lisp/"))
 
 (use-package telega
-  :bind (:map global-map ("s-t" . telega))
-  :config (telega-mode-line-mode 1))
+  :bind (:map global-map ("s-c" . (lambda (p) (interactive "P")
+                                    (if p (call-interactively #'telega-chat-with)
+                                      (telega))))
+         :map telega-chat-button-map ("a" . ignore))
+  :config (telega-mode-line-mode 1)
+  :custom
+  (telega-emoji-use-images nil)
+  (telega-completing-read-function #'completing-read))
 
 (use-package terraform-mode)
-(use-package toml-mode)
+(use-package toml-ts-mode)
 
-(use-package tvl
-  :custom
-  (tvl-gerrit-remote "gerrit"))
+(use-package treecrumbs
+  :hook ((yaml-ts-mode . treecrumbs-mode)))
+
+(use-package tvl)
+
+(use-package vertico
+  :config
+  (vertico-mode))
 
 (use-package web-mode)
-(use-package yaml-mode)
+(use-package yaml-ts-mode)
+(use-package zoxide)
+
+(use-package passively
+  :custom
+  (passively-store-state "/persist/tazjin/known-russian-words.el"))
+
+;; Note taking configuration for deft.
+(use-package deft
+  :custom
+  (deft-directory "/persist/tazjin/deft/")
+  (deft-extensions '("md" "org" "txt"))
+  (deft-default-extension "md"))
+
+(use-package zetteldeft
+  :custom
+  ;; Configure for Markdown
+  (zetteldeft-link-indicator "[[")
+  (zetteldeft-link-suffix "]]")
+  (zetteldeft-title-prefix "# ")
+  (zetteldeft-list-prefix "* "))
+
+;; Initialise midnight.el, which by default automatically cleans up
+;; unused buffers at midnight.
+(require 'midnight)
 
 (defgroup tazjin nil
   "Settings related to my configuration")
@@ -273,7 +231,7 @@
 ;; The way this will work for now is that Emacs will *write*
 ;; configuration to the file tracked in my repository, while not
 ;; actually *reading* it from there (unless Emacs is rebuilt).
-(setq custom-file (expand-file-name "~/depot/tools/emacs/config/custom.el"))
+(setq custom-file (f-join depot-path "users" "tazjin" "emacs" "config" "custom.el"))
 (load-library "custom")
 
 (defvar home-dir (expand-file-name "~"))
@@ -288,10 +246,8 @@
                  look-and-feel
                  functions
                  settings
-                 modes
                  bindings
                  eshell-setup))
-(telephone-line-setup)
 (ace-window-display-mode)
 
 ;; If a local configuration library exists, it should be loaded.
@@ -301,4 +257,6 @@
 (if-let (local-file (locate-library "local"))
     (load local-file))
 
+(require 'dottime)
+
 (provide 'init)
diff --git a/users/tazjin/emacs/config/look-and-feel.el b/users/tazjin/emacs/config/look-and-feel.el
index 8cca6e1bf0..b771b4cd03 100644
--- a/users/tazjin/emacs/config/look-and-feel.el
+++ b/users/tazjin/emacs/config/look-and-feel.el
@@ -11,9 +11,6 @@
 (setq ring-bell-function 'ignore)
 (setq initial-scratch-message "")
 
-;; Remember layout changes
-(winner-mode 1)
-
 ;; Usually emacs will run as a proper GUI application, in which case a few
 ;; extra settings are nice-to-have:
 (when window-system
@@ -22,69 +19,39 @@
   (blink-cursor-mode -1))
 
 ;; Configure Emacs fonts.
-(let ((font (if (equal "frog" (s-trim (shell-command-to-string "hostname")))
-                ;; For unclear reasons, frog refuses to render the
-                ;; regular font weight - everything ends up bold,
-                ;; which makes it hard to distinguish e.g. read/unread
-                ;; emails.
-                ;;
-                ;; Semi-bold looks a little different than on vauxhall
-                ;; and other machines, but it's alright.
-                (format "JetBrains Mono Semi Light-%d" 12)
-              (format "JetBrains Mono-%d" 12))))
+(let ((font (format "JetBrains Mono-%d" 12)))
   (setq default-frame-alist `((font . ,font)))
   (set-frame-font font t t))
 
-;; Configure telephone-line
-(defun telephone-misc-if-last-window ()
-  "Renders the mode-line-misc-info string for display in the
-  mode-line if the currently active window is the last one in the
-  frame.
-
-  The idea is to not display information like the current time,
-  load, battery levels on all buffers."
-
-  (when (bottom-right-window-p)
-    (telephone-line-raw mode-line-misc-info t)))
-
-(defun telephone-line-setup ()
-  (telephone-line-defsegment telephone-line-last-window-segment ()
-    (telephone-misc-if-last-window))
-
-  ;; Display the current EXWM workspace index in the mode-line
-  (telephone-line-defsegment telephone-line-exwm-workspace-index ()
-    (when (bottom-right-window-p)
-      (format "[%s]" exwm-workspace-current-index)))
-
-  ;; Define a highlight font for ~ important ~ information in the last
-  ;; window.
-  (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "")
-  (add-to-list 'telephone-line-faces
-               '(highlight . (special-highlight . special-highlight)))
-
-  (setq telephone-line-lhs
-        '((nil . (telephone-line-position-segment))
-          (accent . (telephone-line-buffer-segment))))
-
-  (setq telephone-line-rhs
-        '((accent . (telephone-line-major-mode-segment))
-          (nil . (telephone-line-last-window-segment
-                  telephone-line-exwm-workspace-index))
-
-          ;; TODO(tazjin): lets not do this particular thing while I
-          ;; don't actually run notmuch, there are too many things
-          ;; that have a dependency on the modeline drawing correctly
-          ;; (including randr operations!)
-          ;;
-          ;; (highlight . (telephone-line-notmuch-counts))
-          ))
-
-  (setq telephone-line-primary-left-separator 'telephone-line-tan-left
-        telephone-line-primary-right-separator 'telephone-line-tan-right
-        telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left
-        telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right)
-
-  (telephone-line-mode 1))
+;; Configure the modeline
+
+;; Implements a mode-line warning if there are any logged in TTY
+;; sessions apart from the graphical one.
+;;
+;; The status is only updated once every 30 seconds, as it requires
+;; shelling out to some commands (for now).
+(defun list-tty-sessions ()
+  "List all logged in tty sessions, except tty7 (graphical)"
+  (let ((command "who | awk '{print $2}' | grep -v tty7"))
+    (-filter (lambda (s) (not (string-empty-p s)))
+             (s-lines
+              (s-trim (let ((default-directory "/"))
+                        (shell-command-to-string command)))))))
+
+(defvar cached-tty-sessions (cons (time-convert nil 'integer) (list-tty-sessions))
+   "Cached TTY session value to avoid running the command too often.")
+
+;; TODO(tazjin): add this to the modeline
+
+(defun get-cached-tty-sessions ()
+  (let ((time ))
+    (when (< 30
+             (- (time-convert nil 'integer)
+                (car cached-tty-sessions)))
+      (setq cached-tty-sessions
+            (cons (time-convert nil 'integer) (list-tty-sessions)))))
+
+  (cdr cached-tty-sessions))
 
 ;; Auto refresh buffers
 (global-auto-revert-mode 1)
@@ -119,4 +86,13 @@
 ;; Don't wrap around when moving between buffers
 (setq windmove-wrap-around nil)
 
+;; Don't show me all emacs warnings immediately. Unfortunately this is
+;; not very granular, as emacs displays most of its warnings in the
+;; `emacs' "category", but without it every time I
+;; fullscreen/unfullscreen the warning buffer destroys my layout.
+;;
+;; Warnings suppressed by this are still logged to the warnings
+;; buffer.
+(setq warning-suppress-types '((emacs)))
+
 (provide 'look-and-feel)
diff --git a/users/tazjin/emacs/config/mail-setup.el b/users/tazjin/emacs/config/mail-setup.el
index 1167bcadd3..7352c8ba10 100644
--- a/users/tazjin/emacs/config/mail-setup.el
+++ b/users/tazjin/emacs/config/mail-setup.el
@@ -1,8 +1,6 @@
 (require 'notmuch)
-(require 'counsel-notmuch)
 
 ;; (global-set-key (kbd "C-c m") 'notmuch-hello)
-;; (global-set-key (kbd "C-c C-m") 'counsel-notmuch)
 ;; (global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail)
 
 (setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME")))
@@ -25,8 +23,10 @@
 (setq notmuch-show-empty-saved-searches t)
 
 ;; Mail sending configuration
-(setq send-mail-function 'sendmail-send-it) ;; sendmail provided by MSMTP
-(setq notmuch-always-prompt-for-sender t)
+(setq sendmail-program "gmi") ;; lieer binary supports sendmail emulation
+(setq message-sendmail-extra-arguments
+      '("send" "--quiet" "-t" "-C" "~/mail/account.tazjin"))
+(setq send-mail-function 'sendmail-send-it)
 (setq notmuch-mua-user-agent-function
       (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version)))
 (setq mail-host-address (system-name))
@@ -49,7 +49,7 @@
 ;; handle that gracefully.
 (define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore)
 
-;; Define a telephone-line segment for displaying the count of unread,
+;; Define a mode-line segment for displaying the count of unread,
 ;; important mails in the last window's mode-line:
 (defvar *last-notmuch-count-redraw* 0)
 (defvar *current-notmuch-count* nil)
@@ -74,10 +74,6 @@
              (not (equal *current-notmuch-count* "I: 0; D: 0")))
     *current-notmuch-count*))
 
-(telephone-line-defsegment telephone-line-notmuch-counts ()
-  "This segment displays the count of unread notmuch messages in
-  the last window's mode-line (if unread messages are present)."
-
-  (update-display-notmuch-counts))
+;; TODO(tazjin): re-add this segment to the modeline
 
 (provide 'mail-setup)
diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el
deleted file mode 100644
index 69fb523d0d..0000000000
--- a/users/tazjin/emacs/config/modes.el
+++ /dev/null
@@ -1,37 +0,0 @@
-;; Initializes modes I use.
-
-(add-hook 'prog-mode-hook 'esk-add-watchwords)
-(add-hook 'prog-mode-hook 'hl-line-mode)
-
-;; Use auto-complete as completion at point
-(defun set-auto-complete-as-completion-at-point-function ()
-  (setq completion-at-point-functions '(auto-complete)))
-
-(add-hook 'auto-complete-mode-hook
-          'set-auto-complete-as-completion-at-point-function)
-
-;; Enable rainbow-delimiters for all things programming
-(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
-
-;; Enable Paredit & Company in Emacs Lisp mode
-(add-hook 'emacs-lisp-mode-hook 'company-mode)
-
-;; Always highlight matching brackets
-(show-paren-mode 1)
-
-;; Always auto-close parantheses and other pairs
-(electric-pair-mode)
-
-;; Keep track of recent files
-(recentf-mode)
-
-;; Easily navigate sillycased words
-(global-subword-mode 1)
-
-;; Transparently open compressed files
-(auto-compression-mode t)
-
-;; Configure go-mode for Go2 Alpha
-(add-to-list 'auto-mode-alist '("\\.go2$" . go-mode))
-
-(provide 'modes)
diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el
index b895d5e406..6c66ca608d 100644
--- a/users/tazjin/emacs/config/settings.el
+++ b/users/tazjin/emacs/config/settings.el
@@ -1,8 +1,5 @@
 (require 'uniquify)
 
-;; Move files to trash when deleting
-(setq delete-by-moving-to-trash t)
-
 ;; We don't live in the 80s, but we're also not a shitty web app.
 (setq gc-cons-threshold 20000000)
 
@@ -48,4 +45,45 @@
 ;; Show time in 24h format
 (setq display-time-24hr-format t)
 
+;; Use python-mode for Starlark files.
+(add-to-list 'auto-mode-alist '("\\.star\\'" . python-mode))
+
+;; Use cmake-mode for relevant files.
+(add-to-list 'auto-mode-alist '("ya\\.make\\'" . cmake-ts-mode))
+
+;; Use tree-sitter modes for various languages.
+(setq major-mode-remap-alist
+      '((bash-mode . bash-ts-mode)
+        (c++-mode . c++-ts-mode)
+        (c-mode . c-ts-mode)
+        (c-or-c++-mode . c-or-c++-ts-mode)
+        (json-mode . json-ts-mode)
+        (python-mode . python-ts-mode)
+        (rust-mode . rust-ts-mode)
+        (toml-mode . toml-ts-mode)
+        (yaml-mode . yaml-ts-mode)
+        (go-mode . go-ts-mode)
+        (cmake-mode . cmake-ts-mode)))
+
+;; Visually highlight current line in programming buffers
+(add-hook 'prog-mode-hook 'hl-line-mode)
+
+;; Enable rainbow-delimiters for all things programming
+(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
+
+;; Always highlight matching brackets
+(show-paren-mode 1)
+
+;; Always auto-close parantheses and other pairs
+(electric-pair-mode)
+
+;; Keep track of recent files
+(recentf-mode)
+
+;; Easily navigate sillycased words
+(global-subword-mode 1)
+
+;; Transparently open compressed files
+(auto-compression-mode t)
+
 (provide 'settings)
diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix
index df73c05ffb..46843432f1 100644
--- a/users/tazjin/emacs/default.nix
+++ b/users/tazjin/emacs/default.nix
@@ -1,153 +1,189 @@
 # This file builds an Emacs pre-configured with the packages I need
 # and my personal Emacs configuration.
-#
-# On NixOS machines, this Emacs currently does not support
-# Imagemagick, see https://github.com/NixOS/nixpkgs/issues/70631.
-#
-# Forcing Emacs to link against Imagemagick currently causes libvterm
-# to segfault, which is a lot less desirable than not having telega
-# render images correctly.
-{ depot, lib, ... }:
+{ depot, lib, pkgs, ... }:
 
-let
-  inherit (depot) third_party;
+pkgs.makeOverridable
+  ({ emacs ? pkgs.emacs29 }:
+  let
+    emacsPackages = (pkgs.emacsPackagesFor emacs);
+    emacsWithPackages = emacsPackages.emacsWithPackages;
 
-  emacsWithPackages = (third_party.emacsPackagesGen third_party.emacs27).emacsWithPackages;
+    # If switching telega versions, use this variable because it will
+    # keep the version check, binary path and so on in sync.
+    currentTelega = epkgs: epkgs.melpaPackages.telega;
 
-  # Pick telega from unstable channel for recent fixes.
-  unstable = import third_party.nixpkgsSrc {};
-  telegaUnstable = (unstable.emacsPackagesGen third_party.emacs27).telega;
+    # $PATH for binaries that need to be available to Emacs
+    emacsBinPath = lib.makeBinPath [
+      (currentTelega pkgs.emacsPackages)
+      pkgs.libwebp # for dwebp, required by telega
+    ];
 
-  # $PATH for binaries that need to be available to Emacs
-  emacsBinPath = lib.makeBinPath [ telegaUnstable ];
+    identity = x: x;
 
-  identity = x: x;
+    # tree-sitter grammars for various ts-modes
+    customTreesitGrammars = emacs.pkgs.treesit-grammars.with-grammars (g: with g; [
+      tree-sitter-bash
+      tree-sitter-c
+      tree-sitter-cmake
+      tree-sitter-cpp
+      tree-sitter-css
+      tree-sitter-dockerfile
+      tree-sitter-go
+      tree-sitter-gomod
+      tree-sitter-hcl
+      tree-sitter-html
+      tree-sitter-java
+      tree-sitter-json
+      tree-sitter-latex
+      tree-sitter-make
+      tree-sitter-nix
+      tree-sitter-python
+      tree-sitter-rust
+      tree-sitter-sql
+      tree-sitter-toml
+      tree-sitter-yaml
+    ]);
 
-  tazjinsEmacs = pkgfun: (emacsWithPackages(epkgs: pkgfun(
-  # Actual ELPA packages (the enlightened!)
-  (with epkgs.elpaPackages; [
-    ace-window
-    avy
-    flymake
-    pinentry
-    rainbow-mode
-    undo-tree
-    xelb
-  ]) ++
+    tazjinsEmacs = pkgfun: (emacsWithPackages (epkgs: pkgfun (with epkgs; [
+      ace-link
+      ace-window
+      avy
+      bazel
+      browse-kill-ring
+      cargo
+      clojure-mode
+      consult
+      deft
+      direnv
+      elixir-mode
+      elm-mode
+      erlang
+      depotExwm
+      go-mode
+      google-c-style
+      gruber-darker-theme
+      haskell-mode
+      ht
+      hydra
+      idle-highlight-mode
+      inspector
+      jq-mode
+      kotlin-mode
+      kubernetes
+      magit
+      markdown-toc
+      multiple-cursors
+      nginx-mode
+      nix-mode
+      notmuch
+      paredit
+      password-store
+      pinentry
+      prescient
+      protobuf-mode
+      rainbow-delimiters
+      rainbow-mode
+      request
+      restclient
+      rust-mode
+      sly
+      string-edit-at-point
+      terraform-mode
+      undo-tree
+      uuidgen
+      vertico
+      vterm
+      web-mode
+      websocket
+      which-key
+      xelb
+      yasnippet
+      zetteldeft
+      zoxide
 
-  # MELPA packages:
-  (with epkgs.melpaPackages; [
-    ace-link
-    bazel-mode
-    browse-kill-ring
-    cargo
-    clojure-mode
-    cmake-mode
-    counsel
-    counsel-notmuch
-    dash-functional
-    direnv
-    dockerfile-mode
-    eglot
-    elixir-mode
-    elm-mode
-    erlang
-    geiser
-    go-mode
-    gruber-darker-theme
-    haskell-mode
-    ht
-    hydra
-    idle-highlight-mode
-    intero
-    ivy
-    ivy-pass
-    ivy-prescient
-    jq-mode
-    kotlin-mode
-    lispy
-    lsp-mode
-    magit
-    markdown-toc
-    meson-mode
-    multi-term
-    multiple-cursors
-    nginx-mode
-    nix-mode
-    notmuch # this comes from pkgs.third_party
-    org-journal
-    org-ql
-    paredit
-    password-store
-    pg
-    polymode
-    prescient
-    protobuf-mode
-    racket-mode
-    rainbow-delimiters
-    refine
-    request
-    restclient
-    sly
-    string-edit
-    swiper
-    telegaUnstable
-    telephone-line
-    terraform-mode
-    toml-mode
-    transient
-    use-package
-    uuidgen
-    web-mode
-    websocket
-    which-key
-    yaml-mode
-    yasnippet
-  ]) ++
+      # experimental (not otherwise embedded in config yet)
+      orderless
+      corfu
+      eat
 
-  # Custom packages
-  (with depot.tools.emacs-pkgs; [
-    dottime
-    nix-util
-    term-switcher
-    tvl
+      # Wonky stuff
+      (currentTelega epkgs)
+      customTreesitGrammars # TODO(tazjin): how is this *supposed* to work?!
 
-    # patched / overridden versions of packages
-    depot.third_party.emacs.exwm
-    depot.third_party.emacs.rcirc
-    depot.third_party.emacs.vterm
-    depot.third_party.emacs.explain-pause-mode
-  ]))));
-in lib.fix(self: l: f: third_party.writeShellScriptBin "tazjins-emacs" ''
-  export PATH="${emacsBinPath}:$PATH"
-  exec ${tazjinsEmacs f}/bin/emacs \
-    --debug-init \
-    --no-site-file \
-    --no-site-lisp \
-    --no-init-file \
-    --directory ${./config} ${if l != null then "--directory ${l}" else ""} \
-    --eval "(require 'init)" $@
-  '' // {
-    # Call overrideEmacs with a function (pkgs -> pkgs) to modify the
-    # packages that should be included in this Emacs distribution.
-    overrideEmacs = f': self l f';
+      # Custom depot packages (either ours, or overridden ones)
+      tvlPackages.dottime
+      tvlPackages.nix-util
+      tvlPackages.passively
+      tvlPackages.rcirc
+      tvlPackages.term-switcher
+      tvlPackages.treecrumbs
+      tvlPackages.tvl
 
-    # Call withLocalConfig with the path to a *folder* containing a
-    # `local.el` which provides local system configuration.
-    withLocalConfig = confDir: self confDir f;
+      # Dynamic/native modules
+      depot.users.tazjin.gio-list-apps
+    ])));
 
-    # Build a derivation that uses the specified local Emacs (i.e.
-    # built outside of Nix) instead
-    withLocalEmacs = emacsBin: third_party.writeShellScriptBin "tazjins-emacs" ''
+    # Tired of telega.el runtime breakages through tdlib
+    # incompatibility. Target to make that a build failure instead.
+    tdlibCheck =
+      let
+        tgEmacs = emacsWithPackages (epkgs: [ (currentTelega epkgs) ]);
+        verifyTdlibVersion = builtins.toFile "verify-tdlib-version.el" ''
+          (require 'telega)
+          (defvar tdlib-version "${pkgs.tdlib.version}")
+          (when (or (version< tdlib-version
+                              telega-tdlib-min-version)
+                    (and telega-tdlib-max-version
+                          (version< telega-tdlib-max-version
+                                    tdlib-version)))
+             (message "Found TDLib version %s, but require %s to %s"
+                     tdlib-version telega-tdlib-min-version telega-tdlib-max-version)
+            (kill-emacs 1))
+        '';
+      in
+      pkgs.runCommand "tdlibCheck" { } ''
+        export PATH="${emacsBinPath}:$PATH"
+        ${tgEmacs}/bin/emacs --script ${verifyTdlibVersion} && touch $out
+      '';
+  in
+  lib.fix
+    (self: l: f: (pkgs.writeShellScriptBin "tazjins-emacs" ''
       export PATH="${emacsBinPath}:$PATH"
-      export EMACSLOADPATH="${(tazjinsEmacs f).deps}/share/emacs/site-lisp:"
-      exec ${emacsBin} \
+      exec ${tazjinsEmacs f}/bin/emacs \
         --debug-init \
         --no-site-file \
         --no-site-lisp \
         --no-init-file \
-        --directory ${./config} \
-        ${if l != null then "--directory ${l}" else ""} \
+        --directory ${./config} ${if l != null then "--directory ${l}" else ""} \
+        --eval "(add-to-list 'treesit-extra-load-path \"${customTreesitGrammars}/lib\")" \
         --eval "(require 'init)" $@
-    '';
-  }) null identity
+    '').overrideAttrs
+      (_: {
+        passthru = {
+          # Expose original Emacs used for my configuration.
+          inherit emacs;
+
+          # Expose the pure emacs with all packages.
+          inherit emacsPackages;
+          emacsWithPackages = tazjinsEmacs f;
+
+          # Call overrideEmacs with a function (pkgs -> pkgs) to modify the
+          # packages that should be included in this Emacs distribution.
+          overrideEmacs = f': self l f';
+
+          # Call withLocalConfig with the path to a *folder* containing a
+          # `local.el` which provides local system configuration.
+          withLocalConfig = confDir: self confDir f;
+
+          # Expose telega/tdlib version check as a target that is built in
+          # CI.
+          #
+          # TODO(tazjin): uncomment when telega works again
+          inherit tdlibCheck;
+          meta.ci.targets = [ "tdlibCheck" ];
+        };
+      }))
+    null
+    identity
+  )
+{ }