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 | 107 | ||||
-rw-r--r-- | users/tazjin/emacs/config/custom.el | 27 | ||||
-rw-r--r-- | users/tazjin/emacs/config/desktop.el | 360 | ||||
-rw-r--r-- | users/tazjin/emacs/config/eshell-setup.el | 68 | ||||
-rw-r--r-- | users/tazjin/emacs/config/functions.el | 354 | ||||
-rw-r--r-- | users/tazjin/emacs/config/init.el | 271 | ||||
-rw-r--r-- | users/tazjin/emacs/config/look-and-feel.el | 149 | ||||
-rw-r--r-- | users/tazjin/emacs/config/mail-setup.el | 83 | ||||
-rw-r--r-- | users/tazjin/emacs/config/modes.el | 35 | ||||
-rw-r--r-- | users/tazjin/emacs/config/settings.el | 68 | ||||
-rw-r--r-- | users/tazjin/emacs/default.nix | 183 |
13 files changed, 1723 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..17740b1950a7 --- /dev/null +++ b/users/tazjin/emacs/config/bindings.el @@ -0,0 +1,107 @@ +;; 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) + +;; 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 "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 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) + +;; 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) + +;; 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) + +;; 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. +;; +;; 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 + +;; 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 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..51a3f208481d --- /dev/null +++ b/users/tazjin/emacs/config/desktop.el @@ -0,0 +1,360 @@ +;; -*- 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 'ring) +(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")) + ;; 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))) + + ;; For other Chromium windows, make the title shorter. + (`("Chromium-browser" ,title) + (format "Chromium<%s>" (s-truncate 42 (s-chop-suffix " - Chromium" 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-ring* (make-ring 5) + "Ring of recently used EXWM workspaces.") + +(defvar *workspace-ring-is-rotating* nil + "Variable used to track whether the workspace ring is rotating, +and suppress insertions into the ring in that case.") + +(defun update-recent-workspaces () + "Hook run on EXWM workspace switches, adding new workspaces to the +ring." + + (unless *workspace-ring-is-rotating* + (ring-remove+insert+extend *recent-workspaces-ring* exwm-workspace-current-index))) + +(add-to-list 'exwm-workspace-switch-hook #'update-recent-workspaces) + +(defun switch-to-previous-workspace () + "Switch to the previous workspace in the workspace ring." + (interactive) + + (when-let ((*workspace-ring-is-rotating* t) + (previous (condition-case err (ring-next *recent-workspaces-ring* + exwm-workspace-current-index) + ('error (message "No previous workspace in history!") nil)))) + (exwm-workspace-switch previous))) + +(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) + + (when-let ((*workspace-ring-is-rotating* t) + (next (condition-case err (ring-previous *recent-workspaces-ring* + exwm-workspace-current-index) + ('error (message "No next workspace in history!") nil)))) + (exwm-workspace-switch next))) + +(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") #'run-xdg-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 8) + ("eDP" 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")) + +(defun randr-khamovnik-layout-office () + "Use the left and right screen on khamovnik, in the office." + (interactive) + (set-randr-config `(("eDP-1" 1 2) + ("DP-2" 3 4 5 6 7 8 9 0))) + + (shell-command "xrandr --output DP-2 --mode 2560x1440 --primary --right-of eDP-1") + (exwm-randr-refresh)) + +(defun randr-khamovnik-layout-home () + "Use the left and right screen on khamovnik, at home." + (interactive) + (set-randr-config `(("HDMI-1" 1 2 3 4 5 6 7 8) + ("eDP-1" 9 0))) + + (shell-command "xrandr --output HDMI-1 --auto --primary --left-of eDP-1") + (exwm-randr-refresh)) + +(defun randr-khamovnik-layout-single () + "Use only the internal screen." + (interactive) + (set-randr-config '(("eDP-1" (number-sequence 0 9)))) + (shell-command "xrandr --output eDP-1 --auto --primary") + (shell-command "xrandr --output DP-2 --off") + (shell-command "xrandr --output HDMI-1 --off") + (exwm-randr-refresh)) + +(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)) + + ("khamovnik" + (exwm-input-set-key (kbd "s-m 2") #'randr-khamovnik-layout-office) + (exwm-input-set-key (kbd "s-m s") #'randr-khamovnik-layout-single))) + +;; Notmuch shortcuts as EXWM globals +;; (g m => gmail) +(exwm-input-set-key (kbd "s-g m") #'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..3739d25122ef --- /dev/null +++ b/users/tazjin/emacs/config/functions.el @@ -0,0 +1,354 @@ +(require 'chart) +(require 'dash) +(require 'map) + +(require 'gio-list-apps) ;; native module! + +(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)))) + +;; 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 (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)))) + +(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 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 screenshot-select (filename) + "Take a screenshot based on a mouse-selection and save it to + ~/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. + 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 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." + (magit-find-file--internal "{worktree}" + (magit-read-file-from-rev "HEAD" "Find file") + #'pop-to-buffer-same-window)) + +(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)) + +(provide 'functions) diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el new file mode 100644 index 000000000000..f3a6c555815d --- /dev/null +++ b/users/tazjin/emacs/config/init.el @@ -0,0 +1,271 @@ +;;; 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) + +(package-initialize) + +;; Initialise all packages installed via Nix. + +(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 consult + :bind + ("C-c r g" . consult-ripgrep) + ("C-s" . consult-line)) + +(use-package dash) +(use-package gruber-darker-theme) + +(use-package eglot + :custom + (eglot-autoshutdown t) + (eglot-send-changes-idle-time 0.3)) + +(use-package ht) + +(use-package hydra) +(use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode))) + +(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 + :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-at-point) + +(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 + :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 +;; 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-ts-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 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" . (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) + :hook (telega-chat-mode . company-mode)) + +(use-package terraform-mode) +(use-package toml-ts-mode) + +(use-package tvl) + +(use-package vertico + :config + (vertico-mode)) + +(use-package web-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") + +(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 (f-join depot-path "users" "tazjin" "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..467316aa8b25 --- /dev/null +++ b/users/tazjin/emacs/config/look-and-feel.el @@ -0,0 +1,149 @@ +;;; -*- 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 "") + +;; 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 (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))) + +;; 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 (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.") + +(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)) + +(telephone-line-defsegment telephone-line-warn-tty-session () + (when-let (sessions (get-cached-tty-sessions)) + (format "W: [%s]!!" (s-join "," sessions)))) + +(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 + '((highlight . (telephone-line-warn-tty-session)) + (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..1d7ab6d0b018 --- /dev/null +++ b/users/tazjin/emacs/config/mail-setup.el @@ -0,0 +1,83 @@ +(require 'notmuch) + +;; (global-set-key (kbd "C-c m") 'notmuch-hello) +;; (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..7a51e674772b --- /dev/null +++ b/users/tazjin/emacs/config/modes.el @@ -0,0 +1,35 @@ +;; 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 +;; TODO(tazjin): what is this? +(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) + +(provide 'modes) diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el new file mode 100644 index 000000000000..0ab15a5ac6bf --- /dev/null +++ b/users/tazjin/emacs/config/settings.el @@ -0,0 +1,68 @@ +(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) + +;; 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))) + +(provide 'settings) diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix new file mode 100644 index 000000000000..de21cc590846 --- /dev/null +++ b/users/tazjin/emacs/default.nix @@ -0,0 +1,183 @@ +# This file builds an Emacs pre-configured with the packages I need +# and my personal Emacs configuration. +{ depot, lib, pkgs, ... }: + +pkgs.makeOverridable + ({ emacs ? pkgs.emacs29 }: + 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; + + # 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 (with epkgs; [ + ace-link + ace-window + avy + bazel + browse-kill-ring + cargo + clojure-mode + company + consult + deft + direnv + elixir-mode + elm-mode + erlang + exwm + 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 + telephone-line + terraform-mode + undo-tree + uuidgen + vertico + vterm + web-mode + websocket + which-key + xelb + yasnippet + zetteldeft + zoxide + + # Wonky stuff + (currentTelega epkgs) + customTreesitGrammars # TODO(tazjin): how is this *supposed* to work?! + + # Custom depot packages (either ours, or overridden ones) + tvlPackages.dottime + tvlPackages.nix-util + tvlPackages.passively + tvlPackages.rcirc + tvlPackages.term-switcher + tvlPackages.tvl + + # Dynamic/native modules + depot.users.tazjin.gio-list-apps + ]))); + + # 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" + 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 "(add-to-list 'treesit-extra-load-path \"${customTreesitGrammars}/lib\")" \ + --eval "(require 'init)" $@ + '').overrideAttrs + (_: { + passthru = { + # Expose original Emacs used for my configuration. + inherit emacs; + + # Expose the pure emacs with all packages. + 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 + ) +{ } |