diff options
Diffstat (limited to 'users/tazjin/emacs')
-rw-r--r-- | users/tazjin/emacs/.gitignore | 11 | ||||
-rw-r--r-- | users/tazjin/emacs/README.md | 7 | ||||
-rw-r--r-- | users/tazjin/emacs/config/bindings.el | 65 | ||||
-rw-r--r-- | users/tazjin/emacs/config/custom.el | 27 | ||||
-rw-r--r-- | users/tazjin/emacs/config/desktop.el | 372 | ||||
-rw-r--r-- | users/tazjin/emacs/config/eshell-setup.el | 68 | ||||
-rw-r--r-- | users/tazjin/emacs/config/functions.el | 343 | ||||
-rw-r--r-- | users/tazjin/emacs/config/init.el | 295 | ||||
-rw-r--r-- | users/tazjin/emacs/config/look-and-feel.el | 131 | ||||
-rw-r--r-- | users/tazjin/emacs/config/mail-setup.el | 85 | ||||
-rw-r--r-- | users/tazjin/emacs/config/modes.el | 37 | ||||
-rw-r--r-- | users/tazjin/emacs/config/settings.el | 48 | ||||
-rw-r--r-- | users/tazjin/emacs/default.nix | 177 |
13 files changed, 1666 insertions, 0 deletions
diff --git a/users/tazjin/emacs/.gitignore b/users/tazjin/emacs/.gitignore new file mode 100644 index 000000000000..7b666905f847 --- /dev/null +++ b/users/tazjin/emacs/.gitignore @@ -0,0 +1,11 @@ +.smex-items +*token* +auto-save-list/ +clones/ +elpa/ +irc.el +local.el +other/ +scripts/ +themes/ +*.elc diff --git a/users/tazjin/emacs/README.md b/users/tazjin/emacs/README.md new file mode 100644 index 000000000000..5c667333962e --- /dev/null +++ b/users/tazjin/emacs/README.md @@ -0,0 +1,7 @@ +tools/emacs +=========== + +This sub-folder builds my Emacs configuration, supplying packages from +Nix and configuration from this folder. + +I use Emacs for many things (including as my desktop environment). diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el new file mode 100644 index 000000000000..916d9477568c --- /dev/null +++ b/users/tazjin/emacs/config/bindings.el @@ -0,0 +1,65 @@ +;; 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) + +;; Window switching. (C-x o goes to the next window) +(windmove-default-keybindings) ;; Shift+direction + +;; Start eshell or switch to it if it's active. +(global-set-key (kbd "C-x m") 'eshell) + +(global-set-key (kbd "C-x C-p") 'browse-repositories) +(global-set-key (kbd "M-g M-g") 'goto-line-with-feedback) + +;; Miscellaneous editing commands +(global-set-key (kbd "C-c w") 'whitespace-cleanup) +(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!) +(global-set-key (kbd "C-c b p") 'browse-url-at-point) +(global-set-key (kbd "C-c b b") 'browse-url) + +;; C-x REALLY QUIT (idea by @magnars) +(global-set-key (kbd "C-x r q") 'save-buffers-kill-terminal) +(global-set-key (kbd "C-x C-c") 'ignore) + +;; Open a file in project: +(global-set-key (kbd "C-c f") 'project-find-file) + +;; Search in a project +(global-set-key (kbd "C-c r g") 'rg-in-project) + +;; Open a file via magit: +(global-set-key (kbd "C-c C-f") #'magit-find-file-worktree) + +;; 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 repo through zoxide +(global-set-key (kbd "s-s r") #'zoxide-open-magit) + +;; Add subthread collapsing to notmuch-show. +;; +;; C-, closes a thread, C-. opens a thread. This mirrors stepping +;; in/out of definitions. +(define-key notmuch-show-mode-map (kbd "C-,") 'notmuch-show-open-or-close-subthread) +(define-key notmuch-show-mode-map (kbd "C-.") + (lambda () + (interactive) + (notmuch-show-open-or-close-subthread t))) ;; open + +(provide 'bindings) diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el new file mode 100644 index 000000000000..91eaf69ae59b --- /dev/null +++ b/users/tazjin/emacs/config/custom.el @@ -0,0 +1,27 @@ +(custom-set-variables + ;; custom-set-variables 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. + '(ac-auto-show-menu 0.8) + '(ac-delay 0.2) + '(avy-background t) + '(cargo-process--enable-rust-backtrace 1) + '(company-auto-complete (quote (quote company-explicit-action-p))) + '(company-idle-delay 0.5) + '(custom-safe-themes + (quote + ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default))) + '(display-time-default-load-average nil) + '(display-time-interval 30) + '(elnode-send-file-program "/run/current-system/sw/bin/cat") + '(frame-brackground-mode (quote dark)) + '(global-auto-complete-mode t) + '(kubernetes-commands-display-buffer-function (quote display-buffer)) + '(lsp-gopls-server-path "/home/tazjin/go/bin/gopls") + '(magit-log-show-gpg-status t) + '(ns-alternate-modifier (quote none)) + '(ns-command-modifier (quote control)) + '(ns-right-command-modifier (quote meta)) + '(require-final-newline (quote visit-save)) + '(tls-program (quote ("gnutls-cli --x509cafile %t -p %p %h")))) diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el new file mode 100644 index 000000000000..15c0b1f5e107 --- /dev/null +++ b/users/tazjin/emacs/config/desktop.el @@ -0,0 +1,372 @@ +;; -*- lexical-binding: t; -*- +;; +;; Configure desktop environment settings, including both +;; window-management (EXWM) as well as additional system-wide +;; commands. + +(require 'dash) +(require 'exwm) +(require 'exwm-config) +(require 'exwm-randr) +(require 'exwm-systemtray) +(require 'exwm-xim ) +(require 'f) +(require 's) + +(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)) + (message "Volume command: %s" cmd)) + +(defun volume-mute () (interactive) (pactl "set-sink-mute @DEFAULT_SINK@ toggle")) +(defun volume-up () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ +5%")) +(defun volume-down () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ -5%")) + +(defun brightness-up () + (interactive) + (shell-command tazjin--backlight-increase-command) + (message "Brightness increased")) + +(defun brightness-down () + (interactive) + (shell-command tazjin--backlight-decrease-command) + (message "Brightness decreased")) + +(defun set-xkb-layout (layout) + "Set the current X keyboard layout." + + (shell-command (format "setxkbmap %s" layout)) + (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. + + A lot of commonly used applications either create titles that + are too long by default, or in the case of web + applications (such as Cider) end up being constructed in + awkward ways. + + To avoid this issue, some rewrite rules are applied for more + 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))) + (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider V" title))) title)) + (format "Cider V<%s>" (s-chop-suffix " - Cider V" 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)))) + + ;; Quassel buffers + ;; + ;; These have a title format that looks like: + ;; "Quassel IRC - #tvl (hackint) — Quassel IRC" + (`("quassel" ,title) + (progn + (if (string-match + (rx "Quassel IRC - " + (group (one-or-more (any alnum "[" "]" "&" "-" "#"))) ;; <-- channel name + " (" (group (one-or-more (any ascii space))) ")" ;; <-- network name + " — Quassel IRC") + title) + (format "Quassel<%s>" (match-string 2 title)) + title))) + + ;; For any other application, a name is constructed from the + ;; window's class and name. + (`(,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))))) + (add-hook 'exwm-update-class-hook titlef) + (add-hook 'exwm-update-title-hook titlef)) + +(fringe-mode 3) +(exwm-enable) + +;; Create 10 EXWM workspaces +(setq exwm-workspace-number 10) + +;; 's-N': Switch to certain workspace, but switch back to the previous +;; one when tapping twice (emulates i3's `back_and_forth' feature) +(defvar *exwm-workspace-from-to* '(-1 . -1)) +(defun exwm-workspace-switch-back-and-forth (target-idx) + ;; If the current workspace is the one we last jumped to, and we are + ;; asked to jump to it again, set the target back to the previous + ;; one. + (when (and (eq exwm-workspace-current-index (cdr *exwm-workspace-from-to*)) + (eq target-idx exwm-workspace-current-index)) + (setq target-idx (car *exwm-workspace-from-to*))) + + (setq *exwm-workspace-from-to* + (cons exwm-workspace-current-index target-idx)) + + (exwm-workspace-switch-create target-idx)) + +(dotimes (i 10) + (exwm-input-set-key (kbd (format "s-%d" i)) + `(lambda () + (interactive) + (exwm-workspace-switch-back-and-forth ,i)))) + +;; Implement MRU functionality for EXWM workspaces, making it possible +;; to jump to the previous/next workspace very easily. +(defvar *recent-workspaces* nil + "List of the most recently used EXWM workspaces.") + +(defvar *workspace-jumping-to* nil + "What offset in the workspace history are we jumping to?") + +(defvar *workspace-history-position* 0 + "Where in the workspace history are we right now?") + +(defun update-recent-workspaces () + "Hook to run on every workspace switch which will prepend the new +workspace to the MRU list, unless we are already on that +workspace. Does not affect the MRU list if a jump is +in-progress." + + (if *workspace-jumping-to* + (setq *workspace-history-position* *workspace-jumping-to* + *workspace-jumping-to* nil) + + ;; reset the history position to the front on a normal jump + (setq *workspace-history-position* 0) + + (unless (eq exwm-workspace-current-index (car *recent-workspaces*)) + (setq *recent-workspaces* (cons exwm-workspace-current-index + (-take 9 *recent-workspaces*)))))) + +(add-to-list 'exwm-workspace-switch-hook #'update-recent-workspaces) + +(defun switch-to-previous-workspace () + "Switch to the previous workspace in the MRU workspace list." + (interactive) + + (let* (;; the previous workspace is one position further down in the + ;; workspace history + (position (+ *workspace-history-position* 1)) + (target-idx (elt *recent-workspaces* position))) + (if (not target-idx) + (message "No previous workspace in history!") + + (setq *workspace-jumping-to* position) + (exwm-workspace-switch target-idx)))) + +(exwm-input-set-key (kbd "s-b") #'switch-to-previous-workspace) + +(defun switch-to-next-workspace () + "Switch to the next workspace in the MRU workspace list." + (interactive) + + (if (= *workspace-history-position* 0) + (message "No next workspace in history!") + (let* (;; The next workspace is one position further up in the + ;; history. This always exists unless someone messed with + ;; it. + (position (- *workspace-history-position* 1)) + (target-idx (elt *recent-workspaces* position))) + (setq *workspace-jumping-to* position) + (exwm-workspace-switch target-idx)))) + +(exwm-input-set-key (kbd "s-f") #'switch-to-next-workspace) + +;; Provide a binding for jumping to a buffer on a workspace. +(defun exwm-jump-to-buffer () + "Jump to a workspace on which the target buffer is displayed." + (interactive) + (let ((exwm-layout-show-all-buffers nil) + (initial exwm-workspace-current-index)) + (call-interactively #'exwm-workspace-switch-to-buffer) + ;; After jumping, update the back-and-forth list like on a direct + ;; index jump. + (when (not (eq initial exwm-workspace-current-index)) + (setq *exwm-workspace-from-to* + (cons initial exwm-workspace-current-index))))) + +(exwm-input-set-key (kbd "C-c j") #'exwm-jump-to-buffer) + +;; 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") #'run-external-command) +(exwm-input-set-key (kbd "s-p") #'password-store-lookup) + +;; Add X11 terminal selector to a key +(exwm-input-set-key (kbd "C-x t") #'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) + +;; Volume keys +(exwm-input-set-key (kbd "<XF86AudioMute>") #'volume-mute) +(exwm-input-set-key (kbd "<XF86AudioRaiseVolume>") #'volume-up) +(exwm-input-set-key (kbd "<XF86AudioLowerVolume>") #'volume-down) + +;; Brightness keys +(exwm-input-set-key (kbd "<XF86MonBrightnessDown>") #'brightness-down) +(exwm-input-set-key (kbd "<XF86MonBrightnessUp>") #'brightness-up) +(exwm-input-set-key (kbd "<XF86Display>") #'lock-screen) + +;; Shortcuts for switching between keyboard layouts +(defmacro bind-xkb (lang key) + `(exwm-input-set-key (kbd (format "s-%s" ,key)) + (lambda () + (interactive) + (set-xkb-layout ,lang)))) + +(bind-xkb "us" "k u") +(bind-xkb "de" "k d") +(bind-xkb "no" "k n") +(bind-xkb "ru" "k r") +(bind-xkb "se" "k s") + +;; 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))) + +;; Show time & battery status in the mode line +(display-time-mode) +(display-battery-mode) + +;; enable display of X11 system tray within Emacs +(exwm-systemtray-enable) + +;; Configure xrandr (multi-monitor setup). + +(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 Tverskoy (X13 AMD laptop) +(defun randr-tverskoy-layout-single () + "Laptop screen only!" + (interactive) + (set-randr-config '(("eDP" (number-sequence 0 9)))) + (shell-command "xrandr --output eDP --auto --primary") + (shell-command "xrandr --output HDMI-A-0 --off") + (exwm-randr-refresh)) + +(defun randr-tverskoy-split-workspace () + "Split the workspace across two screens, assuming external to the left." + (interactive) + (set-randr-config + '(("HDMI-A-0" 1 2 3 4 5 6 7) + ("eDP" 8 9 0))) + + (shell-command "xrandr --output HDMI-A-0 --left-of eDP --auto") + (exwm-randr-refresh)) + +(defun randr-tverskoy-tv () + "Split off a workspace to the TV over HDMI." + (interactive) + (set-randr-config + '(("eDP" 1 2 3 4 5 6 7 8 9) + ("HDMI-A-0" 0))) + + (shell-command "xrandr --output HDMI-A-0 --left-of eDP --mode 1920x1080") + (exwm-randr-refresh)) + +;; Layouts for frog (desktop) + +(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")) + +(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 "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")) + ("tverskoy" + (exwm-input-set-key (kbd "s-m s") #'randr-tverskoy-layout-single) + (exwm-input-set-key (kbd "s-m 2") #'randr-tverskoy-split-workspace)) + + ("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))) + +;; Notmuch shortcuts as EXWM globals +;; (g m => gmail) +(exwm-input-set-key (kbd "s-g m") #'notmuch) +(exwm-input-set-key (kbd "s-g M") #'counsel-notmuch) + +(exwm-randr-enable) + +;; Let buffers move seamlessly between workspaces by making them +;; accessible in selectors on all frames. +(setq exwm-workspace-show-all-buffers t) +(setq exwm-layout-show-all-buffers t) + +(provide 'desktop) diff --git a/users/tazjin/emacs/config/eshell-setup.el b/users/tazjin/emacs/config/eshell-setup.el new file mode 100644 index 000000000000..0b23c5a2d1bc --- /dev/null +++ b/users/tazjin/emacs/config/eshell-setup.el @@ -0,0 +1,68 @@ +;; EShell configuration + +(require 'eshell) + +;; Generic settings +;; Hide banner message ... +(setq eshell-banner-message "") + +;; Prompt configuration +(defun clean-pwd (path) + "Turns a path of the form /foo/bar/baz into /f/b/baz + (inspired by fish shell)" + (let* ((hpath (replace-regexp-in-string home-dir + "~" + path)) + (current-dir (split-string hpath "/")) + (cdir (last current-dir)) + (head (butlast current-dir))) + (concat (mapconcat (lambda (s) + (if (string= "" s) nil + (substring s 0 1))) + head + "/") + (if head "/" nil) + (car cdir)))) + +(defun vcprompt (&optional args) + "Call the external vcprompt command with optional arguments. + VCPrompt" + (replace-regexp-in-string + "\n" "" + (shell-command-to-string (concat "vcprompt" args)))) + +(defmacro with-face (str &rest properties) + `(propertize ,str 'face (list ,@properties))) + +(defun prompt-f () + "EShell prompt displaying VC info and such" + (concat + (with-face (concat (clean-pwd (eshell/pwd)) " ") :foreground "#96a6c8") + (if (= 0 (user-uid)) + (with-face "#" :foreground "#f43841") + (with-face "$" :foreground "#73c936")) + (with-face " " :foreground "#95a99f"))) + + +(setq eshell-prompt-function 'prompt-f) +(setq eshell-highlight-prompt nil) +(setq eshell-prompt-regexp "^.+? \\((\\(git\\|svn\\|hg\\|darcs\\|cvs\\|bzr\\):.+?) \\)?[$#] ") + +;; Ignore version control folders in autocompletion +(setq eshell-cmpl-cycle-completions nil + eshell-save-history-on-exit t + eshell-cmpl-dir-ignore "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\)/\\'") + +;; Load some EShell extensions +(eval-after-load 'esh-opt + '(progn + (require 'em-term) + (require 'em-cmpl) + ;; More visual commands! + (add-to-list 'eshell-visual-commands "ssh") + (add-to-list 'eshell-visual-commands "tail") + (add-to-list 'eshell-visual-commands "sl"))) + +(setq eshell-directory-name "~/.config/eshell/") + +(provide 'eshell-setup) diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el new file mode 100644 index 000000000000..602809138eef --- /dev/null +++ b/users/tazjin/emacs/config/functions.el @@ -0,0 +1,343 @@ +(require 'chart) +(require 'dash) +(require 'map) + +(defun load-file-if-exists (filename) + (if (file-exists-p filename) + (load filename))) + +(defun goto-line-with-feedback () + "Show line numbers temporarily, while prompting for the line number input" + (interactive) + (unwind-protect + (progn + (setq-local display-line-numbers t) + (let ((target (read-number "Goto line: "))) + (avy-push-mark) + (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)))) + +(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 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) + (let ((expr (concat "with import <nixos> {}; " derivation))) + (s-chomp (shell-command-to-string (concat "nix-build -E '" expr "'"))))) + +(defun insert-nix-store-path () + (interactive) + (let ((derivation (read-string "Derivation name (in <nixos>): "))) + (insert (nix-store-path derivation)))) + +(defun toggle-force-newline () + "Buffer-local toggle for enforcing final newline on save." + (interactive) + (setq-local require-final-newline (not require-final-newline)) + (message "require-final-newline in buffer %s is now %s" + (buffer-name) + require-final-newline)) + +(defun list-external-commands () + "Creates a list of all external commands available on $PATH + while filtering NixOS wrappers." + (cl-loop + for dir in (split-string (getenv "PATH") path-separator) + when (and (file-exists-p dir) (file-accessible-directory-p dir)) + for lsdir = (cl-loop for i in (directory-files dir t) + for bn = (file-name-nondirectory i) + when (and (not (s-contains? "-wrapped" i)) + (not (member bn completions)) + (not (file-directory-p i)) + (file-executable-p i)) + collect bn) + append lsdir into completions + finally return (sort completions 'string-lessp))) + +(defvar external-command-flag-overrides + '(("google-chrome" . "--force-device-scale-factor=1.4")) + + "This setting lets me add additional flags to specific commands + that are run interactively via `run-external-command'.") + +(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))) + (cmd (if extra-flags (s-join " " (list cmd extra-flags)) cmd))) + (message "Starting %s..." cmd) + (set-process-sentinel + (start-process-shell-command cmd nil cmd) + (lambda (process event) + (when (string= event "finished\n") + (message "%s process finished." process)))))) + +(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))) + (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) + + (let* ((entry (completing-read "Copy password of entry: " + (password-store-list (or password-store-dir + (password-store-dir))) + nil ;; predicate + t ;; require-match + )) + (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 browse-repositories () + "Select a git repository and open its associated magit buffer." + + (interactive) + (magit-status + (completing-read "Repository: " (magit-list-repos)))) + +(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)))) + +(defhydra mc/mark-more-hydra (:color pink) + ("<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" + "Remove the cursor furthest down")) + ("<right>" mc/mmlte--right (if (eq mc/mark-more-like-this-extended-direction 'up) + "Remove the cursor furthest up" + "Skip past the cursor furthest down")) + ("f" nil "Finish selecting")) + +;; Mute the message that mc/mmlte wants to print on its own +(advice-add 'mc/mmlte--message :around (lambda (&rest args) (ignore))) + +(defun mc/mark-dwim (arg) + "Select multiple things, but do what I mean." + + (interactive "p") + (if (not (region-active-p)) (mc/mark-next-lines arg) + (if (< 1 (count-lines (region-beginning) + (region-end))) + (mc/edit-lines arg) + ;; The following is almost identical to `mc/mark-more-like-this-extended', + ;; but uses a hydra (`mc/mark-more-hydra') instead of a transient key map. + (mc/mmlte--down) + (mc/mark-more-hydra/body)))) + +(setq mc/cmds-to-run-for-all '(kill-region paredit-newline)) + +(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 memespace-region () + "Make a meme out of it." + + (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)))) + +(defun insert-todo-comment (prefix todo) + "Insert a comment at point with something for me to do." + + (interactive "P\nsWhat needs doing? ") + (save-excursion + (move-end-of-line nil) + (insert (format " %s TODO(%s): %s" + (s-trim-right comment-start) + (if prefix (read-string "Who needs to do this? ") + (getenv "USER")) + todo)))) + +;; Custom text scale adjustment functions that operate on the entire instance +(defun modify-text-scale (factor) + (set-face-attribute 'default nil + :height (+ (* factor 5) (face-attribute 'default :height)))) + +(defun increase-default-text-scale (prefix) + "Increase default text scale in all Emacs frames, or just the + current frame if PREFIX is set." + + (interactive "P") + (if prefix (text-scale-increase 1) + (modify-text-scale 1))) + +(defun decrease-default-text-scale (prefix) + "Increase default text scale in all Emacs frames, or just the + current frame if PREFIX is set." + + (interactive "P") + (if prefix (text-scale-decrease 1) + (modify-text-scale -1))) + +(defun set-default-text-scale (prefix &optional to) + "Set the default text scale to the specified value, or the + default. Restores current frame's text scale only, if PREFIX is + set." + + (interactive "P") + (if prefix (text-scale-adjust 0) + (set-face-attribute 'default nil :height (or to 120)))) + +(defun scrot-select () + "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/'")) + +(defun graph-unread-mails () + "Create a bar chart of unread mails based on notmuch tags. + Certain tags are excluded from the overview." + + (interactive) + (let ((tag-counts + (-keep (-lambda ((name . search)) + (let ((count + (string-to-number + (s-trim + (notmuch-command-to-string "count" search "and" "tag:unread"))))) + (when (>= count 1) (cons name count)))) + (notmuch-hello-generate-tag-alist '("unread" "signed" "attachment" "important"))))) + + (chart-bar-quickie + (if (< (length tag-counts) 6) + 'vertical 'horizontal) + "Unread emails" + (-map #'car tag-counts) "Tag:" + (-map #'cdr tag-counts) "Count:"))) + +(defun notmuch-show-open-or-close-subthread (&optional prefix) + "Open or close the subthread from (and including) the message at point." + (interactive "P") + (save-excursion + (let ((current-depth (map-elt (notmuch-show-get-message-properties) :depth 0))) + (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties) prefix) + until (or (not (notmuch-show-goto-message-next)) + (= (map-elt (notmuch-show-get-message-properties) :depth) current-depth))))) + (force-window-update)) + +(defun vterm-send-ctrl-x () + "Sends `C-x' to the libvterm." + (interactive) + (vterm-send-key "x" nil nil t)) + +(defun find-depot-project (dir) + "Function used in the `project-find-functions' hook list to + determine the current project root of a depot project." + (when (s-starts-with? "/depot" dir) + (if (f-exists-p (f-join dir "default.nix")) + (cons 'transient dir) + (find-depot-project (f-parent dir))))) + +(add-to-list 'project-find-functions #'find-depot-project) + +(defun magit-find-file-worktree () + (interactive) + "Find a file in the current (ma)git worktree." + (magit-find-file--internal "{worktree}" + (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 rg-in-project (&optional prefix) + "Interactively call ripgrep in the current project, or fall + back to ripgrep default behaviour if prefix is set." + (interactive "P") + (counsel-rg nil (unless prefix + (if-let ((pr (project-current))) + (project-root pr))))) + +(defun zoxide-open-magit () + "Query Zoxide for paths and open magit in the result." + (interactive) + (zoxide-open-with nil #'magit-status-setup-buffer)) + +(provide 'functions) diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el new file mode 100644 index 000000000000..a4160707303f --- /dev/null +++ b/users/tazjin/emacs/config/init.el @@ -0,0 +1,295 @@ +;;; 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)) + :config + (setq aw-keys '(?f ?j ?d ?k ?s ?l ?a) + aw-scope 'frame)) + +(use-package auth-source-pass :config (auth-source-pass-enable)) + +(use-package avy + :bind (("M-j" . avy-goto-char) + ("M-p" . avy-pop-mark) + ("M-g g" . avy-goto-line))) + +(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)) + +(use-package dash) +(use-package gruber-darker-theme) + +(use-package eglot + :custom + (eglot-autoshutdown t) + (eglot-send-changes-idle-time 0.3)) + +(use-package elfeed + :config + (setq elfeed-feeds + '("https://lobste.rs/rss" + "https://www.anti-spiegel.ru/feed/" + "https://www.reddit.com/r/lockdownskepticism/.rss" + "https://www.reddit.com/r/rust/.rss" + "https://news.ycombinator.com/rss" + ("https://xkcd.com/atom.xml" media) + + ;; vlogcreations + ("https://www.youtube.com/feeds/videos.xml?channel_id=UCR0VLWitB2xM4q7tjkoJUPw" media) + ))) + +(use-package ht) + +(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-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 + :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))) + +(use-package pinentry + :config + (setq epa-pinentry-mode 'loopback) + (pinentry-start)) + +(use-package prescient + :after (ivy counsel) + :config (prescient-persist-mode)) + +(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) +(use-package rainbow-mode) +(use-package s) +(use-package string-edit) + +(use-package swiper + :after (counsel ivy) + :bind (("C-s" . swiper))) + +(use-package telephone-line) ;; configuration happens outside of use-package +(use-package term-switcher) + +(use-package undo-tree + :config (global-undo-tree-mode) + :custom (undo-tree-auto-save-history nil)) + +(use-package uuidgen) +(use-package which-key :config (which-key-mode t)) + +;; +;; Applications in emacs +;; + +(use-package magit + :bind ("C-c g" . magit-status) + :config (setq magit-repository-directories '(("/home/tazjin/projects" . 2) + ("/home/tazjin" . 1)))) + +(use-package password-store) +(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))) + +;; vterm removed the ability to set a custom title generator function +;; via the public API, so this overrides its private title generation +;; function instead +(defun vterm--set-title (title) + (rename-buffer + (generate-new-buffer-name + (format "vterm<%s>" + (s-trim-left + (s-chop-prefix "fish" title)))))) + +;; +;; Packages providing language-specific functionality +;; + +(use-package cargo + :hook ((rust-mode . cargo-minor-mode) + (cargo-process-mode . visual-line-mode)) + :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore))) + +(use-package dockerfile-mode) + +(use-package erlang + :hook ((erlang-mode . (lambda () + ;; Don't indent after '>' while I'm writing + (local-set-key ">" 'self-insert-command))))) + +(use-package f) + +(use-package go-mode + :bind (:map go-mode-map ("C-c C-r" . recompile)) + :hook ((go-mode . (lambda () + (setq tab-width 2) + (setq-local compile-command + (concat "go build " buffer-file-name)))))) + +(use-package haskell-mode) + +(use-package ielm + :hook ((inferior-emacs-lisp-mode . (lambda () + (paredit-mode) + (rainbow-delimiters-mode-enable) + (company-mode))))) + +(use-package jq-mode + :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode))) + +(use-package kotlin-mode + :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 '("\\.markdown\\'" . markdown-mode)) + (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode))) + +(use-package markdown-toc) + +(use-package nix-mode + :hook ((nix-mode . (lambda () + (setq indent-line-function #'nix-indent-line))))) + +(use-package nix-util) +(use-package nginx-mode) +(use-package rust-mode) + +(use-package sly + :hook ((sly-mrepl-mode . (lambda () + (paredit-mode) + (rainbow-delimiters-mode-enable) + (company-mode)))) + :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) + (add-hook 'telega-msg-ignore-predicates 'telega-msg-from-blocked-sender-p)) + +(use-package terraform-mode) +(use-package toml-mode) + +(use-package tvl) + +(use-package web-mode) +(use-package yaml-mode) +(use-package zoxide) + +(use-package passively + :custom + (passively-store-state "/persist/tazjin/known-russian-words.el")) + +;; Initialise midnight.el, which by default automatically cleans up +;; unused buffers at midnight. +(require 'midnight) + +(defgroup tazjin nil + "Settings related to my configuration") + +(defcustom depot-path "/depot" + "Local path to the depot checkout" + :group 'tazjin) + +;; Configuration changes in `customize` can not actually be persisted +;; to the customise file that Emacs is currently using (since it comes +;; from the Nix store). +;; +;; 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")) +(load-library "custom") + +(defvar home-dir (expand-file-name "~")) + +;; Seed RNG +(random t) + +;; Load all other Emacs configuration. These configurations are +;; added to `load-path' by Nix. +(mapc 'require '(desktop + mail-setup + 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. +;; +;; This can be provided by calling my Emacs derivation with +;; `withLocalConfig'. +(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 new file mode 100644 index 000000000000..72665d00c67f --- /dev/null +++ b/users/tazjin/emacs/config/look-and-feel.el @@ -0,0 +1,131 @@ +;;; -*- lexical-binding: t; -*- + +;; Hide those ugly tool bars: +(tool-bar-mode 0) +(scroll-bar-mode 0) +(menu-bar-mode 0) +(add-hook 'after-make-frame-functions + (lambda (frame) (scroll-bar-mode 0))) + +;; Don't do any annoying things: +(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 + (setq frame-title-format '(buffer-file-name "%f" ("%b"))) + (mouse-wheel-mode t) + (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)))) + (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)) + +;; Auto refresh buffers +(global-auto-revert-mode 1) + +;; Use clipboard properly +(setq select-enable-clipboard t) + +;; Show in-progress chords in minibuffer +(setq echo-keystrokes 0.1) + +;; Show column numbers in all buffers +(column-number-mode t) + +(defalias 'yes-or-no-p 'y-or-n-p) +(defalias 'auto-tail-revert-mode 'tail-mode) + +;; Style line numbers (shown with M-g g) +(setq linum-format + (lambda (line) + (propertize + (format (concat " %" + (number-to-string + (length (number-to-string + (line-number-at-pos (point-max))))) + "d ") + line) + 'face 'linum))) + +;; Display tabs as 2 spaces +(setq tab-width 2) + +;; 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 new file mode 100644 index 000000000000..7fbece1b102a --- /dev/null +++ b/users/tazjin/emacs/config/mail-setup.el @@ -0,0 +1,85 @@ +(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"))) +(make-directory notmuch-cache-dir t) + +;; Cache addresses for completion: +(setq notmuch-address-save-filename (concat notmuch-cache-dir "/addresses")) + +;; Don't spam my home folder with drafts: +(setq notmuch-draft-folder "drafts") ;; relative to notmuch database + +;; Mark things as read when archiving them: +(setq notmuch-archive-tags '("-inbox" "-unread" "+archive")) + +;; Show me saved searches that I care about: +(setq notmuch-saved-searches + '((:name "inbox" :query "tag:inbox" :count-query "tag:inbox AND tag:unread" :key "i") + (:name "sent" :query "tag:sent" :key "t") + (:name "drafts" :query "tag:draft"))) +(setq notmuch-show-empty-saved-searches t) + +;; Mail sending configuration +(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)) +(setq notmuch-mua-cite-function #'message-cite-original-without-signature) +(setq notmuch-fcc-dirs nil) ;; Gmail does this server-side +(setq message-signature nil) ;; Insert message signature manually with C-c C-w + +;; Close mail buffers after sending mail +(setq message-kill-buffer-on-exit t) + +;; Ensure sender is correctly passed to msmtp +(setq mail-specify-envelope-from t + message-sendmail-envelope-from 'header + mail-envelope-from 'header) + +;; Store sent mail in the correct folder per account +(setq notmuch-maildir-use-notmuch-insert nil) + +;; I don't use drafts but I instinctively hit C-x C-s constantly, lets +;; 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, +;; important mails in the last window's mode-line: +(defvar *last-notmuch-count-redraw* 0) +(defvar *current-notmuch-count* nil) + +(defun update-display-notmuch-counts () + "Update and render the current state of the notmuch unread + count for display in the mode-line. + + The offlineimap-timer runs every 2 minutes, so it does not make + sense to refresh this much more often than that." + + (when (> (- (float-time) *last-notmuch-count-redraw*) 30) + (setq *last-notmuch-count-redraw* (float-time)) + (let* ((inbox-unread (notmuch-saved-search-count "tag:inbox and tag:unread")) + (notmuch-count (format "I: %s; D: %s" inbox-unread))) + (setq *current-notmuch-count* notmuch-count))) + + (when (and (bottom-right-window-p) + ;; Only render if the initial update is done and there + ;; are unread mails: + *current-notmuch-count* + (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)) + +(provide 'mail-setup) diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el new file mode 100644 index 000000000000..69fb523d0d91 --- /dev/null +++ b/users/tazjin/emacs/config/modes.el @@ -0,0 +1,37 @@ +;; 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 new file mode 100644 index 000000000000..8b15b6cda183 --- /dev/null +++ b/users/tazjin/emacs/config/settings.el @@ -0,0 +1,48 @@ +(require 'uniquify) + +;; We don't live in the 80s, but we're also not a shitty web app. +(setq gc-cons-threshold 20000000) + +(setq uniquify-buffer-name-style 'forward) + +; Fix some defaults +(setq visible-bell nil + inhibit-startup-message t + color-theme-is-global t + sentence-end-double-space nil + shift-select-mode nil + uniquify-buffer-name-style 'forward + whitespace-style '(face trailing lines-tail tabs) + whitespace-line-column 80 + default-directory "~" + fill-column 80 + ediff-split-window-function 'split-window-horizontally + initial-major-mode 'emacs-lisp-mode) + +(add-to-list 'safe-local-variable-values '(lexical-binding . t)) +(add-to-list 'safe-local-variable-values '(whitespace-line-column . 80)) + +(set-default 'indent-tabs-mode nil) + +;; UTF-8 please +(setq locale-coding-system 'utf-8) ; pretty +(set-terminal-coding-system 'utf-8) ; pretty +(set-keyboard-coding-system 'utf-8) ; pretty +(set-selection-coding-system 'utf-8) ; please +(prefer-coding-system 'utf-8) ; with sugar on top + +;; Make emacs behave sanely (overwrite selected text) +(delete-selection-mode 1) + +;; Keep your temporary files in tmp, emacs! +(setq auto-save-file-name-transforms + `((".*" ,temporary-file-directory t))) +(setq backup-directory-alist + `((".*" . ,temporary-file-directory))) + +(remove-hook 'kill-buffer-query-functions 'server-kill-buffer-query-function) + +;; Show time in 24h format +(setq display-time-24hr-format t) + +(provide 'settings) diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix new file mode 100644 index 000000000000..90cb6ef29982 --- /dev/null +++ b/users/tazjin/emacs/default.nix @@ -0,0 +1,177 @@ +# This file builds an Emacs pre-configured with the packages I need +# and my personal Emacs configuration. +{ lib, pkgs, ... }: + +pkgs.makeOverridable + ({ emacs ? pkgs.emacsNativeComp }: + let + emacsWithPackages = (pkgs.emacsPackagesFor emacs).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; + + # $PATH for binaries that need to be available to Emacs + emacsBinPath = lib.makeBinPath [ + (currentTelega pkgs.emacsPackages) + pkgs.libwebp # for dwebp, required by telega + ]; + + identity = x: x; + + tazjinsEmacs = pkgfun: (emacsWithPackages (epkgs: pkgfun (with epkgs; [ + ace-link + ace-window + avy + bazel + browse-kill-ring + cargo + clojure-mode + cmake-mode + company + counsel + counsel-notmuch + d-mode + direnv + dockerfile-mode + eglot + elfeed + elixir-mode + elm-mode + erlang + exwm + flymake + go-mode + google-c-style + gruber-darker-theme + haskell-mode + ht + hydra + idle-highlight-mode + ivy + ivy-prescient + jq-mode + kotlin-mode + lsp-mode + magit + markdown-toc + meson-mode + multi-term + multiple-cursors + nginx-mode + nix-mode + notmuch + paredit + password-store + pinentry + polymode + prescient + protobuf-mode + rainbow-delimiters + rainbow-mode + refine + request + restclient + rust-mode + sly + string-edit + swiper + telephone-line + terraform-mode + toml-mode + transient + undo-tree + use-package + uuidgen + vterm + web-mode + websocket + which-key + xelb + yaml-mode + yasnippet + zoxide + + # Wonky stuff + (currentTelega epkgs) + + # Custom depot packages (either ours, or overridden ones) + tvlPackages.dottime + tvlPackages.nix-util + tvlPackages.passively + tvlPackages.rcirc + tvlPackages.term-switcher + tvlPackages.tvl + ]))); + + # 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.runCommandNoCC "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" + 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)" $@ + '').overrideAttrs + (_: { + passthru = { + # 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; + + # Build a derivation that uses the specified local Emacs (i.e. + # built outside of Nix) instead + withLocalEmacs = emacsBin: pkgs.writeShellScriptBin "tazjins-emacs" '' + export PATH="${emacsBinPath}:$PATH" + export EMACSLOADPATH="${(tazjinsEmacs f).deps}/share/emacs/site-lisp:" + exec ${emacsBin} \ + --debug-init \ + --no-site-file \ + --no-site-lisp \ + --no-init-file \ + --directory ${./config} \ + ${if l != null then "--directory ${l}" else ""} \ + --eval "(require 'init)" $@ + ''; + + # 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 + ) +{ } |