diff options
Diffstat (limited to 'users/tazjin/emacs/config')
-rw-r--r-- | users/tazjin/emacs/config/bindings.el | 65 | ||||
-rw-r--r-- | users/tazjin/emacs/config/custom.el | 6 | ||||
-rw-r--r-- | users/tazjin/emacs/config/desktop.el | 257 | ||||
-rw-r--r-- | users/tazjin/emacs/config/functions.el | 195 | ||||
-rw-r--r-- | users/tazjin/emacs/config/init.el | 154 | ||||
-rw-r--r-- | users/tazjin/emacs/config/look-and-feel.el | 102 | ||||
-rw-r--r-- | users/tazjin/emacs/config/mail-setup.el | 10 | ||||
-rw-r--r-- | users/tazjin/emacs/config/modes.el | 37 | ||||
-rw-r--r-- | users/tazjin/emacs/config/settings.el | 44 |
9 files changed, 460 insertions, 410 deletions
diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el index 916d947756..d8b63e33e4 100644 --- a/users/tazjin/emacs/config/bindings.el +++ b/users/tazjin/emacs/config/bindings.el @@ -1,11 +1,11 @@ +;; Switch buffers reliably in the face of spurious renames. +(global-set-key (kbd "C-x b") #'reliably-switch-buffer) + ;; Font size (define-key global-map (kbd "C-=") 'increase-default-text-scale) ;; '=' because there lies '+' (define-key global-map (kbd "C--") 'decrease-default-text-scale) (define-key global-map (kbd "C-x C-0") 'set-default-text-scale) -;; What does <tab> do? Well, it depends ... -(define-key prog-mode-map (kbd "<tab>") #'company-indent-or-complete-common) - ;; imenu instead of insert-file (global-set-key (kbd "C-x i") 'imenu) @@ -15,7 +15,6 @@ ;; 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 @@ -23,7 +22,7 @@ (global-set-key (kbd "C-c a") 'align-regexp) (global-set-key (kbd "C-c m") 'mc/mark-dwim) -;; Browse URLs (very useful for Gitlab's SSH output!) +;; Browse URLs (very useful for Gerrit's push output, etc!) (global-set-key (kbd "C-c b p") 'browse-url-at-point) (global-set-key (kbd "C-c b b") 'browse-url) @@ -34,23 +33,17 @@ ;; 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) +;; Open any project through zoxide +(global-set-key (kbd "s-s r") #'zoxide-open-project) ;; Add subthread collapsing to notmuch-show. ;; @@ -62,4 +55,50 @@ (interactive) (notmuch-show-open-or-close-subthread t))) ;; open +;; Get rid of the annoying `save-some-buffers' shortcut which I +;; *NEVER* use intentionally. +(unbind-key (kbd "C-x s") 'global-map) + +;; German keyboard layout with Y and Z in the correct place. + +(quail-define-package + "german-qwerty" "German" "DE@" t + "German (Deutsch) input method with QWERTY keys" + nil t t t t nil nil nil nil nil t) + +;; 1! 2" 3§ 4$ 5% 6& 7/ 8( 9) 0= ß? [{ ]} +;; qQ wW eE rR tT yY uU iI oO pP üÜ +* +;; aA sS dD fF gG hH jJ kK lL öÖ äÄ #^ +;; zZ xX cC vV bB nN mM ,; .: -_ + +(quail-define-rules + ("-" ?ß) + ("=" ?\[) + ("`" ?\]) + ("[" ?ü) + ("]" ?+) + (";" ?ö) + ("'" ?ä) + ("\\" ?#) + ("/" ?-) + + ("@" ?\") + ("#" ?§) + ("^" ?&) + ("&" ?/) + ("*" ?\() + ("(" ?\)) + (")" ?=) + ("_" ??) + ("+" ?{) + ("~" ?}) + ("{" ?Ü) + ("}" ?*) + (":" ?Ö) + ("\"" ?Ä) + ("|" ?^) + ("<" ?\;) + (">" ?:) + ("?" ?_)) + (provide 'bindings) diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el index 2bb7ad4896..3e9a9dcd06 100644 --- a/users/tazjin/emacs/config/custom.el +++ b/users/tazjin/emacs/config/custom.el @@ -7,9 +7,9 @@ '(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-enabled-themes (quote (gruber-darker))) + '(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") diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el index c160ae131f..aa232fec2f 100644 --- a/users/tazjin/emacs/config/desktop.el +++ b/users/tazjin/emacs/config/desktop.el @@ -4,14 +4,15 @@ ;; 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) +(require 'seq) (defcustom tazjin--screen-lock-command "tazjin-screen-lock" "Command to execute for locking the screen." @@ -69,36 +70,17 @@ 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)))) + ;; 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)))) + + ;; similarly for Firefox + (`("firefox" ,title) + (format "FF<%s>" title)) ;; Quassel buffers ;; @@ -120,9 +102,6 @@ (`(,class ,title) (format "%s<%s>" class (s-truncate 12 title))))) ;; EXWM launch configuration -;; -;; This used to use use-package, but when something breaks use-package -;; it doesn't exactly make debugging any easier. (let ((titlef (lambda () (exwm-workspace-rename-buffer (create-window-name))))) @@ -130,23 +109,57 @@ (add-hook 'exwm-update-title-hook titlef)) (fringe-mode 3) + +;; tab-bar related config +(setq tab-bar-show 1) +(setq tab-bar-tab-hints t) + +(setq tab-bar-format + '(tab-bar-format-history + tab-bar-format-tabs tab-bar-separator + tab-bar-format-align-right tab-bar-format-global)) + +(setq tab-bar-new-tab-choice + (lambda () (get-buffer-create "*scratch*"))) + +(tab-bar-mode 1) + +(setq x-no-window-manager t) ;; TODO(tazjin): figure out when to remove this (exwm-enable) +(exwm-randr-enable) -;; 's-N': Switch to certain workspace -(setq exwm-workspace-number 10) -(dotimes (i 10) - (exwm-input-set-key (kbd (format "s-%d" i)) - `(lambda () - (interactive) - (exwm-workspace-switch-create ,i)))) +;; Tab-management shortcuts + +(defun tab-bar-select-or-return () + "This function behaves like `tab-bar-select-tab', except it calls +`tab-recent' if asked to jump to the current tab. This simulates +the back&forth behaviour of i3." + (interactive) + (let* ((key (event-basic-type last-command-event)) + (tab (if (and (characterp key) (>= key ?1) (<= key ?9)) + (- key ?0) + 0)) + (current (1+ (tab-bar--current-tab-index)))) + (if (eq tab current) + (tab-recent) + (tab-bar-select-tab tab)))) + +(dotimes (i 8) + (exwm-input-set-key (kbd (format "s-%d" (+ 1 i))) #'tab-bar-select-or-return)) + +(exwm-input-set-key (kbd "s-9") #'tab-last) +(exwm-input-set-key (kbd "s-f") #'tab-next) +(exwm-input-set-key (kbd "s-b") #'tab-recent) +(exwm-input-set-key (kbd "s-w") #'tab-close) +(exwm-input-set-key (kbd "s-n") #'tab-new) ;; Launch applications / any command with completion (dmenu style!) -(exwm-input-set-key (kbd "s-d") #'counsel-linux-app) +(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) +;; Add vterm selector to a key +(exwm-input-set-key (kbd "s-v") #'ts/switch-to-terminal) ;; Toggle between line-mode / char-mode (exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard) @@ -173,10 +186,6 @@ (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" "л т") @@ -188,9 +197,8 @@ (push ?\C-\\ exwm-input-prefix-keys) ;; Line-editing shortcuts -(exwm-input-set-simulation-keys - '(([?\C-d] . delete) - ([?\C-w] . ?\C-c))) +(exwm-input-set-simulation-key (kbd "C-d") (kbd "DEL")) +(exwm-input-set-simulation-key (kbd "C-w") (kbd "C-c")) ;; Show time & battery status in the mode line (display-time-mode) @@ -199,66 +207,123 @@ ;; 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)))) +;; Multi-monitor configuration. +;; +;; With tab-bar-mode, each monitor only displays at most one +;; workspace. Workspaces are only created, never deleted, meaning that +;; the number of workspaces will be equivalent to the maximum number +;; of displays that were connected during a session. +;; +;; The first workspace is special: It is kept on the primary monitor. -;; Layouts for Tverskoy (X13 AMD laptop) -(defun randr-tverskoy-layout-single () - "Laptop screen only!" +(defun exwm-assign-workspaces () + "Assigns workspaces to the currently existing monitors, putting +the first one on the primary display and allocating the others +dynamically if needed in no particular order." + (interactive) + (let* ((randr-monitors (exwm-randr--get-monitors)) + (primary (car randr-monitors)) + (all-monitors (seq-map #'car (cadr randr-monitors))) + (sorted-primary-first (seq-sort (lambda (a b) + (or (equal a primary) + (< a b))) + all-monitors)) + ;; assign workspace numbers to each monitor ... + (workspace-assignments + (flatten-list (seq-map-indexed (lambda (monitor idx) + (list idx monitor)) + sorted-primary-first)))) + ;; ensure that the required workspaces exist + (exwm-workspace-switch-create (- (seq-length all-monitors) 1)) + + ;; update randr config + (setq exwm-randr-workspace-monitor-plist workspace-assignments) + (exwm-randr-refresh) + + ;; leave focus on primary workspace + (exwm-workspace-switch 0))) + +(defun list-available-monitors () + "List connected, but unused monitors." + (let* ((all-connected + (seq-map (lambda (line) (car (s-split " " line))) + (s-lines (s-trim (shell-command-to-string "xrandr | grep connected | grep -v disconnected"))))) + (all-active (seq-map #'car (cadr (exwm-randr--get-monitors))))) + (seq-filter (lambda (s) (not (seq-contains-p all-active s))) + all-connected))) + +(defun exwm-enable-monitor () + "Interactively construct an EXWM invocation that enable the +given monitor and assigns a workspace to it." (interactive) - (set-randr-config '(("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." + (let* ((monitors (list-available-monitors)) + (primary (car (exwm-randr--get-monitors))) + (monitor (pcase (seq-length monitors) + (0 (error "No available monitors.")) + (1 (car monitors)) + (_ + (completing-read "Which monitor? " (list-available-monitors) nil t)))) + + (configurations `(("secondary (left)" . ,(format "--left-of %s" primary)) + ("secondary (right)" . ,(format "--right-of %s" primary)) + ("primary (left)" . ,(format "--left-of %s --primary" primary)) + ("primary (right)" . ,(format "--right-of %s --primary" primary)) + ("mirror" . ,(format "--same-as %s" primary)))) + + (where (completing-read (format "%s should be " monitor) + (seq-map #'car configurations) + nil t)) + (xrandr-pos (cdr (assoc where configurations))) + (xrandr-cmd (format "xrandr --output %s --auto %s" monitor xrandr-pos))) + (message "Invoking '%s'" xrandr-cmd) + (shell-command xrandr-cmd) + (exwm-assign-workspaces))) + +(defun exwm-disable-monitor () + "Interactively choose a monitor to disable." (interactive) - (set-randr-config - '(("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)) + (let* ((all (exwm-randr--get-monitors)) + (active (seq-map #'car (cadr all))) + (monitor (if (> (seq-length active) 1) + (completing-read "Disable which monitor? " active nil t) + (error "Only one monitor is active!"))) -;; Layouts for frog (desktop) + ;; If this monitor was primary, pick another active one instead. + (remaining (seq-filter (lambda (s) (not (equal s monitor))) active)) + (new-primary + (when (equal monitor (car all)) + (pcase (seq-length remaining) + (1 (car remaining)) + (_ (completing-read "New primary? " remaining nil t)))))) -(defun randr-frog-layout-right-only () - "Use only the right screen on frog." - (interactive) - (set-randr-config `(("DisplayPort-0" ,(number-sequence 0 9)))) - (shell-command "xrandr --output DisplayPort-0 --off") - (shell-command "xrandr --output DisplayPort-1 --auto --primary")) + (when new-primary + (shell-command (format "xrandr --output %s --primary" new-primary))) -(defun randr-frog-layout-both () - "Use the left and right screen on frog." - (interactive) - (set-randr-config `(("DisplayPort-0" 1 2 3 4 5) - ("DisplayPort-1" 6 7 8 9 0))) + (shell-command (format "xrandr --output %s --off" monitor)) + (exwm-assign-workspaces))) - (shell-command "xrandr --output DisplayPort-0 --auto --primary --left-of DisplayPort-1") - (shell-command "xrandr --output DisplayPort-1 --auto --right-of DisplayPort-0 --rotate left")) +(defun exwm-switch-monitor () + "Switch focus to another monitor by name." + (interactive) -(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)) + ;; TODO: Filter out currently active? How to determine it? + (let* ((target (completing-read "Switch to monitor: " + (seq-map #'car (cadr (exwm-randr--get-monitors))) + nil t)) + (target-workspace + (cl-loop for (workspace screen) on exwm-randr-workspace-monitor-plist by #'cddr + when (equal screen target) return workspace))) + (exwm-workspace-switch target-workspace))) - ("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))) +(exwm-input-set-key (kbd "s-m e") #'exwm-enable-monitor) +(exwm-input-set-key (kbd "s-m d") #'exwm-disable-monitor) +(exwm-input-set-key (kbd "s-m o") #'exwm-switch-monitor) ;; Notmuch shortcuts as EXWM globals ;; (g m => gmail) (exwm-input-set-key (kbd "s-g m") #'notmuch) -(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. diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el index ba7301e794..68a384d20f 100644 --- a/users/tazjin/emacs/config/functions.el +++ b/users/tazjin/emacs/config/functions.el @@ -2,9 +2,7 @@ (require 'dash) (require 'map) -(defun load-file-if-exists (filename) - (if (file-exists-p filename) - (load filename))) +(require 'gio-list-apps) ;; native module! (defun goto-line-with-feedback () "Show line numbers temporarily, while prompting for the line number input" @@ -17,24 +15,19 @@ (goto-line target))) (setq-local display-line-numbers nil))) -;; These come from the emacs starter kit - (defun esk-add-watchwords () (font-lock-add-keywords nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)" 1 font-lock-warning-face t)))) +(add-hook 'prog-mode-hook 'esk-add-watchwords) + (defun esk-sudo-edit (&optional arg) (interactive "p") (if (or arg (not buffer-file-name)) (find-file (concat "/sudo:root@localhost:" (read-file-name "File: "))) (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name)))) -;; Open 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) @@ -114,7 +107,9 @@ the GPG agent correctly." nil ;; predicate t ;; require-match )) - (password (auth-source-pass-get 'secret entry))) + (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) @@ -124,25 +119,8 @@ the GPG agent correctly." (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>" mmlte--up "Mark previous like this") + ("<up>" mc/mmlte--up "Mark previous like this") ("<down>" mc/mmlte--down "Mark next like this") ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up) "Skip past the cursor furthest up" @@ -168,26 +146,15 @@ the GPG agent correctly." (mc/mmlte--down) (mc/mark-more-hydra/body)))) -(defun memespace-region () - "Make a meme out of it." +(setq mc/cmds-to-run-for-all '(kill-region paredit-newline)) - (interactive) - (let* ((start (region-beginning)) - (end (region-end)) - (memed - (message - (s-trim-right - (apply #'string - (-flatten - (nreverse - (-reduce-from (lambda (acc x) - (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc)) - '() - (string-to-list (buffer-substring-no-properties start end)))))))))) - - (save-excursion (delete-region start end) - (goto-char start) - (insert memed)))) +(setq mc/cmds-to-run-once '(mc/mark-dwim + mc/mark-more-hydra/mc/mmlte--down + mc/mark-more-hydra/mc/mmlte--left + mc/mark-more-hydra/mc/mmlte--right + mc/mark-more-hydra/mc/mmlte--up + mc/mark-more-hydra/mmlte--up + mc/mark-more-hydra/nil)) (defun insert-todo-comment (prefix todo) "Insert a comment at point with something for me to do." @@ -231,11 +198,16 @@ the GPG agent correctly." (if prefix (text-scale-adjust 0) (set-face-attribute 'default nil :height (or to 120)))) -(defun scrot-select () +(defun screenshot-select (filename) "Take a screenshot based on a mouse-selection and save it to ~/screenshots." - (interactive) - (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'")) + (interactive "sScreenshot filename: ") + (let* ((path (f-join "~/screenshots" + (format "%s-%d.png" + (if (string-empty-p filename) "shot" filename) + (time-convert nil 'integer))))) + (shell-command (format "maim --select %s" path)) + (message "Wrote screenshot to %s" path))) (defun graph-unread-mails () "Create a bar chart of unread mails based on notmuch tags. @@ -283,6 +255,17 @@ the GPG agent correctly." (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." @@ -290,44 +273,80 @@ the GPG agent correctly." (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-project () + "Query Zoxide for paths, and open the result as appropriate (magit or dired)." + (interactive) + (zoxide-open-with + nil + (lambda (path) + (condition-case err (magit-status-setup-buffer path) + (magit-outside-git-repo (dired path)))))) + +(defun toggle-nix-test-and-exp () + "Switch between the .nix and .exp file in a Tvix/Nix test." + (interactive) + (let* ((file (buffer-file-name)) + (other (if (s-suffix? ".nix" file) + (s-replace-regexp ".nix$" ".exp" file) + (if (s-suffix? ".exp" file) + (s-replace-regexp ".exp$" ".nix" file) + (error "Not a .nix/.exp file!"))))) + (find-file other))) + +(defun reliably-switch-buffer () + "Reliably and interactively switch buffers, without ending up in a +situation where the buffer was renamed during selection and an +empty new buffer is created. + +This is done by, in contrast to most buffer-switching functions, +retaining a list of the buffer *objects* and their associated +names, instead of only their names (which might change)." + + (interactive) + (let* ((buffers (seq-map (lambda (b) (cons (buffer-name b) b)) + (seq-filter (lambda (b) (not (string-prefix-p " " (buffer-name b)))) + (buffer-list)))) + + ;; Annotate buffers that display remote files. I frequently + ;; want to see it, because I might have identically named + ;; files open locally and remotely at the same time, and it + ;; helps with differentiating them. + (completion-extra-properties + '(:annotation-function + (lambda (name) + (if-let* ((file (buffer-file-name (cdr (assoc name buffers)))) + (remote (file-remote-p file))) + (format " [%s]" remote))))) + + (name (completing-read "Switch to buffer: " (seq-map #'car buffers))) + (selected (or (cdr (assoc name buffers)) + ;; Allow users to manually select invisible buffers ... + (get-buffer name)))) + (switch-to-buffer (or selected name) nil 't))) + +(defun run-xdg-app () + "Use `//users/tazjin/gio-list-apps' to retrieve a list of +installed (and visible) XDG apps, and let users launch them." + (interactive) + (let* ((apps (taz-list-xdg-apps)) + + ;; Display the command that will be run as an annotation + (completion-extra-properties + '(:annotation-function (lambda (app) (format " [%s]" (cdr (assoc app apps))))))) + + (run-external-command--handler (cdr (assoc (completing-read "App: " apps nil t) apps))))) + +(defun advice-remove-all (sym) + "Remove all advices from symbol SYM." + (interactive "aFunction symbol: ") + (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym)) -(defun zoxide-open-magit () - "Query Zoxide for paths and open magit in the result." +(defun M-x-always-same-window () + "Run `execute-extended-command', but ensure that whatever it does +always opens in the same window in which the command was invoked." (interactive) - (zoxide-open-with nil #'magit-status-setup-buffer)) + (let ((display-buffer-overriding-action + '((display-buffer-same-window) . ((inhibit-same-window . nil))))) + (call-interactively #'execute-extended-command))) (provide 'functions) diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el index 27e6312e4e..ced3bf2ff8 100644 --- a/users/tazjin/emacs/config/init.el +++ b/users/tazjin/emacs/config/init.el @@ -10,23 +10,9 @@ (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)) @@ -43,13 +29,10 @@ (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 consult + :bind + ("C-c r g" . consult-ripgrep) + ("C-s" . consult-line)) (use-package dash) (use-package gruber-darker-theme) @@ -59,39 +42,11 @@ (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 @@ -109,33 +64,20 @@ (pinentry-start)) (use-package prescient - :after (ivy counsel) - :config (prescient-persist-mode)) - -(use-package rainbow-delimiters - :hook (prog-mode . rainbow-delimiters-mode) - :custom-face - (rainbow-delimiters-depth-1-face ((t (:foreground "#2aa198")))) - (rainbow-delimiters-depth-2-face ((t (:foreground "#b58900")))) - (rainbow-delimiters-depth-3-face ((t (:foreground "#268bd2")))) - (rainbow-delimiters-depth-4-face ((t (:foreground "#dc322f")))) - (rainbow-delimiters-depth-5-face ((t (:foreground "#859900")))) - (rainbow-delimiters-depth-6-face ((t (:foreground "#268bd2")))) - (rainbow-delimiters-depth-7-face ((t (:foreground "#cb4b16")))) - (rainbow-delimiters-depth-8-face ((t (:foreground "#d33682")))) - (rainbow-delimiters-depth-9-face ((t (:foreground "#839496"))))) + :config + (prescient-persist-mode) + (setq completion-styles '(basic prescient))) +(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) (use-package rainbow-mode) (use-package s) -(use-package string-edit) +(use-package string-edit-at-point) +(use-package term-switcher) -(use-package swiper - :after (counsel ivy) - :bind (("C-s" . swiper))) +(use-package undo-tree + :config (global-undo-tree-mode) + :custom (undo-tree-auto-save-history nil)) -(use-package telephone-line) ;; configuration happens outside of use-package -(use-package term-switcher) -(use-package undo-tree :config (global-undo-tree-mode)) (use-package uuidgen) (use-package which-key :config (which-key-mode t)) @@ -152,20 +94,9 @@ (use-package restclient) (use-package vterm - :config (progn - (setq vterm-shell "fish") - (setq vterm-exit-functions - (lambda (&rest _) (kill-buffer (current-buffer)))) - (setq vterm-kill-buffer-on-exit t)) - :custom-face - (term-color-black ((t (:background "#282828" :foreground "#282828")))) - (term-color-blue ((t (:background "#96a6c8" :foreground "#96a6c8")))) - (term-color-cyan ((t (:background "#1fad83" :foreground "#1fad83")))) - (term-color-green ((t (:background "#73c936" :foreground "#73c936")))) - (term-color-magenta ((t (:background "#9e95c7" :foreground "#9e95c7")))) - (term-color-red ((t (:background "#f43841" :foreground "#f43841")))) - (term-color-white ((t (:background "#f5f5f5" :foreground "#f5f5f5")))) - (term-color-yellow ((t (:background "#ffdd33" :foreground "#ffdd33"))))) + :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 @@ -186,7 +117,7 @@ (cargo-process-mode . visual-line-mode)) :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore))) -(use-package dockerfile-mode) +(use-package dockerfile-ts-mode) (use-package erlang :hook ((erlang-mode . (lambda () @@ -206,9 +137,7 @@ (use-package ielm :hook ((inferior-emacs-lisp-mode . (lambda () - (paredit-mode) - (rainbow-delimiters-mode-enable) - (company-mode))))) + (rainbow-delimiters-mode-enable))))) (use-package jq-mode :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode))) @@ -217,8 +146,6 @@ :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)) @@ -237,30 +164,55 @@ (use-package sly :hook ((sly-mrepl-mode . (lambda () (paredit-mode) - (rainbow-delimiters-mode-enable) - (company-mode)))) + (rainbow-delimiters-mode-enable)))) :config (setq common-lisp-hyperspec-root "file:///home/tazjin/docs/lisp/")) (use-package telega - :bind (:map global-map ("s-t" . telega)) - :config - (telega-mode-line-mode 1) - (add-hook 'telega-msg-ignore-predicates 'telega-msg-from-blocked-sender-p)) + :bind (:map global-map ("s-c" . (lambda (p) (interactive "P") + (if p (call-interactively #'telega-chat-with) + (telega)))) + :map telega-chat-button-map ("a" . ignore)) + :config (telega-mode-line-mode 1) + :custom + (telega-emoji-use-images nil) + (telega-completing-read-function #'completing-read)) (use-package terraform-mode) -(use-package toml-mode) +(use-package toml-ts-mode) + +(use-package treecrumbs + :hook ((yaml-ts-mode . treecrumbs-mode))) (use-package tvl) +(use-package vertico + :config + (vertico-mode)) + (use-package web-mode) -(use-package yaml-mode) +(use-package yaml-ts-mode) (use-package zoxide) (use-package passively :custom (passively-store-state "/persist/tazjin/known-russian-words.el")) +;; Note taking configuration for deft. +(use-package deft + :custom + (deft-directory "/persist/tazjin/deft/") + (deft-extensions '("md" "org" "txt")) + (deft-default-extension "md")) + +(use-package zetteldeft + :custom + ;; Configure for Markdown + (zetteldeft-link-indicator "[[") + (zetteldeft-link-suffix "]]") + (zetteldeft-title-prefix "# ") + (zetteldeft-list-prefix "* ")) + ;; Initialise midnight.el, which by default automatically cleans up ;; unused buffers at midnight. (require 'midnight) @@ -279,7 +231,7 @@ ;; The way this will work for now is that Emacs will *write* ;; configuration to the file tracked in my repository, while not ;; actually *reading* it from there (unless Emacs is rebuilt). -(setq custom-file (expand-file-name "~/depot/tools/emacs/config/custom.el")) +(setq custom-file (f-join depot-path "users" "tazjin" "emacs" "config" "custom.el")) (load-library "custom") (defvar home-dir (expand-file-name "~")) @@ -294,10 +246,8 @@ look-and-feel functions settings - modes bindings eshell-setup)) -(telephone-line-setup) (ace-window-display-mode) ;; If a local configuration library exists, it should be loaded. diff --git a/users/tazjin/emacs/config/look-and-feel.el b/users/tazjin/emacs/config/look-and-feel.el index 8cca6e1bf0..b771b4cd03 100644 --- a/users/tazjin/emacs/config/look-and-feel.el +++ b/users/tazjin/emacs/config/look-and-feel.el @@ -11,9 +11,6 @@ (setq ring-bell-function 'ignore) (setq initial-scratch-message "") -;; Remember layout changes -(winner-mode 1) - ;; Usually emacs will run as a proper GUI application, in which case a few ;; extra settings are nice-to-have: (when window-system @@ -22,69 +19,39 @@ (blink-cursor-mode -1)) ;; Configure Emacs fonts. -(let ((font (if (equal "frog" (s-trim (shell-command-to-string "hostname"))) - ;; For unclear reasons, frog refuses to render the - ;; regular font weight - everything ends up bold, - ;; which makes it hard to distinguish e.g. read/unread - ;; emails. - ;; - ;; Semi-bold looks a little different than on vauxhall - ;; and other machines, but it's alright. - (format "JetBrains Mono Semi Light-%d" 12) - (format "JetBrains Mono-%d" 12)))) +(let ((font (format "JetBrains Mono-%d" 12))) (setq default-frame-alist `((font . ,font))) (set-frame-font font t t)) -;; Configure telephone-line -(defun telephone-misc-if-last-window () - "Renders the mode-line-misc-info string for display in the - mode-line if the currently active window is the last one in the - frame. - - The idea is to not display information like the current time, - load, battery levels on all buffers." - - (when (bottom-right-window-p) - (telephone-line-raw mode-line-misc-info t))) - -(defun telephone-line-setup () - (telephone-line-defsegment telephone-line-last-window-segment () - (telephone-misc-if-last-window)) - - ;; Display the current EXWM workspace index in the mode-line - (telephone-line-defsegment telephone-line-exwm-workspace-index () - (when (bottom-right-window-p) - (format "[%s]" exwm-workspace-current-index))) - - ;; Define a highlight font for ~ important ~ information in the last - ;; window. - (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "") - (add-to-list 'telephone-line-faces - '(highlight . (special-highlight . special-highlight))) - - (setq telephone-line-lhs - '((nil . (telephone-line-position-segment)) - (accent . (telephone-line-buffer-segment)))) - - (setq telephone-line-rhs - '((accent . (telephone-line-major-mode-segment)) - (nil . (telephone-line-last-window-segment - telephone-line-exwm-workspace-index)) - - ;; TODO(tazjin): lets not do this particular thing while I - ;; don't actually run notmuch, there are too many things - ;; that have a dependency on the modeline drawing correctly - ;; (including randr operations!) - ;; - ;; (highlight . (telephone-line-notmuch-counts)) - )) - - (setq telephone-line-primary-left-separator 'telephone-line-tan-left - telephone-line-primary-right-separator 'telephone-line-tan-right - telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left - telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right) - - (telephone-line-mode 1)) +;; Configure the modeline + +;; Implements a mode-line warning if there are any logged in TTY +;; sessions apart from the graphical one. +;; +;; The status is only updated once every 30 seconds, as it requires +;; shelling out to some commands (for now). +(defun list-tty-sessions () + "List all logged in tty sessions, except tty7 (graphical)" + (let ((command "who | awk '{print $2}' | grep -v tty7")) + (-filter (lambda (s) (not (string-empty-p s))) + (s-lines + (s-trim (let ((default-directory "/")) + (shell-command-to-string command))))))) + +(defvar cached-tty-sessions (cons (time-convert nil 'integer) (list-tty-sessions)) + "Cached TTY session value to avoid running the command too often.") + +;; TODO(tazjin): add this to the modeline + +(defun get-cached-tty-sessions () + (let ((time )) + (when (< 30 + (- (time-convert nil 'integer) + (car cached-tty-sessions))) + (setq cached-tty-sessions + (cons (time-convert nil 'integer) (list-tty-sessions))))) + + (cdr cached-tty-sessions)) ;; Auto refresh buffers (global-auto-revert-mode 1) @@ -119,4 +86,13 @@ ;; Don't wrap around when moving between buffers (setq windmove-wrap-around nil) +;; Don't show me all emacs warnings immediately. Unfortunately this is +;; not very granular, as emacs displays most of its warnings in the +;; `emacs' "category", but without it every time I +;; fullscreen/unfullscreen the warning buffer destroys my layout. +;; +;; Warnings suppressed by this are still logged to the warnings +;; buffer. +(setq warning-suppress-types '((emacs))) + (provide 'look-and-feel) diff --git a/users/tazjin/emacs/config/mail-setup.el b/users/tazjin/emacs/config/mail-setup.el index 7fbece1b10..7352c8ba10 100644 --- a/users/tazjin/emacs/config/mail-setup.el +++ b/users/tazjin/emacs/config/mail-setup.el @@ -1,8 +1,6 @@ (require 'notmuch) -(require 'counsel-notmuch) ;; (global-set-key (kbd "C-c m") 'notmuch-hello) -;; (global-set-key (kbd "C-c C-m") 'counsel-notmuch) ;; (global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail) (setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME"))) @@ -51,7 +49,7 @@ ;; handle that gracefully. (define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore) -;; Define a telephone-line segment for displaying the count of unread, +;; Define a mode-line segment for displaying the count of unread, ;; important mails in the last window's mode-line: (defvar *last-notmuch-count-redraw* 0) (defvar *current-notmuch-count* nil) @@ -76,10 +74,6 @@ (not (equal *current-notmuch-count* "I: 0; D: 0"))) *current-notmuch-count*)) -(telephone-line-defsegment telephone-line-notmuch-counts () - "This segment displays the count of unread notmuch messages in - the last window's mode-line (if unread messages are present)." - - (update-display-notmuch-counts)) +;; TODO(tazjin): re-add this segment to the modeline (provide 'mail-setup) diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el deleted file mode 100644 index 69fb523d0d..0000000000 --- a/users/tazjin/emacs/config/modes.el +++ /dev/null @@ -1,37 +0,0 @@ -;; Initializes modes I use. - -(add-hook 'prog-mode-hook 'esk-add-watchwords) -(add-hook 'prog-mode-hook 'hl-line-mode) - -;; Use auto-complete as completion at point -(defun set-auto-complete-as-completion-at-point-function () - (setq completion-at-point-functions '(auto-complete))) - -(add-hook 'auto-complete-mode-hook - 'set-auto-complete-as-completion-at-point-function) - -;; Enable rainbow-delimiters for all things programming -(add-hook 'prog-mode-hook 'rainbow-delimiters-mode) - -;; Enable Paredit & Company in Emacs Lisp mode -(add-hook 'emacs-lisp-mode-hook 'company-mode) - -;; Always highlight matching brackets -(show-paren-mode 1) - -;; Always auto-close parantheses and other pairs -(electric-pair-mode) - -;; Keep track of recent files -(recentf-mode) - -;; Easily navigate sillycased words -(global-subword-mode 1) - -;; Transparently open compressed files -(auto-compression-mode t) - -;; Configure go-mode for Go2 Alpha -(add-to-list 'auto-mode-alist '("\\.go2$" . go-mode)) - -(provide 'modes) diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el index 8b15b6cda1..afe181b70b 100644 --- a/users/tazjin/emacs/config/settings.el +++ b/users/tazjin/emacs/config/settings.el @@ -19,6 +19,9 @@ ediff-split-window-function 'split-window-horizontally initial-major-mode 'emacs-lisp-mode) +(setq-default tab-width 4) +(setq-default fill-column 80) + (add-to-list 'safe-local-variable-values '(lexical-binding . t)) (add-to-list 'safe-local-variable-values '(whitespace-line-column . 80)) @@ -45,4 +48,45 @@ ;; Show time in 24h format (setq display-time-24hr-format t) +;; Use python-mode for Starlark files. +(add-to-list 'auto-mode-alist '("\\.star\\'" . python-mode)) + +;; Use cmake-mode for relevant files. +(add-to-list 'auto-mode-alist '("ya\\.make\\'" . cmake-ts-mode)) + +;; Use tree-sitter modes for various languages. +(setq major-mode-remap-alist + '((bash-mode . bash-ts-mode) + (c++-mode . c++-ts-mode) + (c-mode . c-ts-mode) + (c-or-c++-mode . c-or-c++-ts-mode) + (json-mode . json-ts-mode) + (python-mode . python-ts-mode) + (rust-mode . rust-ts-mode) + (toml-mode . toml-ts-mode) + (yaml-mode . yaml-ts-mode) + (go-mode . go-ts-mode) + (cmake-mode . cmake-ts-mode))) + +;; Visually highlight current line in programming buffers +(add-hook 'prog-mode-hook 'hl-line-mode) + +;; Enable rainbow-delimiters for all things programming +(add-hook 'prog-mode-hook 'rainbow-delimiters-mode) + +;; Always highlight matching brackets +(show-paren-mode 1) + +;; Always auto-close parantheses and other pairs +(electric-pair-mode) + +;; Keep track of recent files +(recentf-mode) + +;; Easily navigate sillycased words +(global-subword-mode 1) + +;; Transparently open compressed files +(auto-compression-mode t) + (provide 'settings) |