auto-update-daily-20260202
1;;; init --- vdemeester's emacs configuration -*- lexical-binding: t -*-
2
3;; Copyright (C) 2025 Vincent Demeester
4;; Author: Vincent Demeester <vincent@sbr.pm>
5
6;; This file is NOT part of GNU Emacs.
7;;; Commentary:
8;; This is the "mini" version for now, but aims to become the default one.
9;;; Code:
10
11;;; Some constants I am using across the configuration.
12(defconst org-directory "~/desktop/org/"
13 "`org-mode' directory, where most of the org-mode file lives.")
14(defconst org-notes-directory (expand-file-name "notes" org-directory)
15 "`org-mode' notes directory, for notes obviously, most likely managed by denote.")
16(defconst org-inbox-file (expand-file-name "inbox.org" org-directory)
17 "`org-mode' inbox file, where we collect entries to be triaged.")
18(defconst org-todos-file (expand-file-name "todos.org" org-directory)
19 "`org-mode' file for TODOs. This is the main file for the org angenda entries.")
20(defconst org-habits-file (expand-file-name "habits.org" org-directory)
21 "`org-mode' file for habits. This is the file for habits that I need to
22share elsewhere, with Flat habits on iOS for example.")
23(defconst org-reading-list-file (expand-file-name "reading-list.org" org-directory)
24 "`org-mode' file for list of things to read.
25Most likely these needs to be added to readwise reader or ditch.")
26(defconst org-calendar-file (expand-file-name "calendar.org" org-directory)
27 "`org-mode' calendar file, auto-generated from Google Calendar (read-only).")
28(defconst org-journelly-file (expand-file-name "Journelly.org" org-directory)
29 "`org-mode' file for journalling.
30It is shared with iOS and replace the deprecated `org-journal-file' below.")
31(defconst org-journal-file (expand-file-name "20250620T144103--journal__journal.org" org-notes-directory)
32 "`org-mode' journal file, for journal-ling.")
33(defconst org-archive-dir (expand-file-name "archive" org-directory)
34 "`org-mode' directory of archived files.")
35(defconst org-people-dir (expand-file-name "people" org-notes-directory)
36 "`org-mode' people files directory, most likely managed by denote.")
37
38;;; The configuration.
39
40;;; Quick access to certain key file using registers
41(set-register ?e `(file . ,(locate-user-emacs-file "init.el")))
42(set-register ?i `(file . ,org-inbox-file))
43(set-register ?t `(file . ,org-todos-file))
44(set-register ?c `(file . ,org-calendar-file))
45(set-register ?j `(file . ,org-journelly-file))
46(set-register ?o `(file . ,org-directory))
47(set-register ?n `(file . ,org-notes-directory))
48(set-register ?P `(file . ,org-people-dir))
49
50;;; Some GC optimizations
51(defun my-minibuffer-setup-hook ()
52 (setq gc-cons-threshold most-positive-fixnum))
53
54(defun my-minibuffer-exit-hook ()
55 (setq gc-cons-threshold 800000000))
56
57(setq gc-cons-threshold most-positive-fixnum)
58
59(run-with-idle-timer 1.2 t 'garbage-collect)
60
61(defconst emacs-start-time (current-time))
62
63(let ((minver 29))
64 (unless (>= emacs-major-version minver)
65 (error "Your Emacs is too old -- this configuration requires v%s or higher" minver)))
66
67(setq inhibit-default-init t) ; Disable the site default settings
68
69(setq confirm-kill-emacs #'y-or-n-p)
70
71(setq custom-file (locate-user-emacs-file "custom.el"))
72(require 'cus-edit)
73(setq
74 custom-buffer-done-kill nil ; Kill when existing
75 custom-buffer-verbose-help nil ; Remove redundant help text
76 custom-unlispify-tag-names nil ; Show me the real variable name
77 custom-unlispify-menu-entries nil)
78;; Create the custom-file if it doesn't exists
79(unless (file-exists-p custom-file)
80 (write-region "" nil custom-file))
81(load custom-file :no-error-if-file-is-missing)
82
83(setq echo-keystrokes 0.1) ;; display command keystrokes quickly
84
85(global-unset-key (kbd "C-z"))
86(global-unset-key (kbd "C-x C-z"))
87(global-unset-key (kbd "C-h h"))
88
89;; Make C-w behave like in terminal: delete word when no region is selected
90(defun kill-region-or-backward-word ()
91 "If the region is active and non-empty, call `kill-region'.
92Otherwise, call `backward-kill-word'."
93 (interactive)
94 (call-interactively
95 (if (use-region-p) 'kill-region 'backward-kill-word)))
96(global-set-key (kbd "C-w") 'kill-region-or-backward-word)
97
98;; Disable owerwrite-mode, iconify-frame and diary
99(mapc
100 (lambda (command)
101 (put command 'disabled t))
102 '(overwrite-mode iconify-frame diary))
103;; And enable those commands (disabled by default)
104(mapc
105 (lambda (command)
106 (put command 'disabled nil))
107 '(list-timers narrow-to-region narrow-to-page upcase-region downcase-region))
108
109(unless noninteractive
110 (defconst font-height 130
111 "Default font-height to use.")
112 ;; 2024-10-05: Switching from Ubuntu Mono to Cascadia Mono
113 ;; 2024-96-06: Switching from Cascadia Mono to JetBrains Mono
114 (defconst font-family-mono "JetBrains Mono"
115 "Default monospace font-family to use.")
116 (defconst font-family-sans "Ubuntu Sans"
117 "Default sans font-family to use.")
118 ;; Middle/Near East: שלום, السّلام عليكم
119 (when (and (fboundp 'set-fontset-font) (member "Noto Sans Arabic" (font-family-list)))
120 (set-fontset-font t 'arabic "Noto Sans Arabic"))
121 (when (and (fboundp 'set-fontset-font) (member "Noto Sans Hebrew" (font-family-list)))
122 (set-fontset-font t 'arabic "Noto Sans Hebrew"))
123 ;; Africa: ሠላም
124 (when (and (fboundp 'set-fontset-font) (member "Noto Sans Ethiopic" (font-family-list)))
125 (set-fontset-font t 'ethiopic "Noto Sans Ethiopic"))
126
127 ;; If font-family-mono or font-family-sans are not available, use the default Emacs face
128 (set-face-attribute 'default nil
129 :family font-family-mono
130 :height font-height
131 :weight 'regular)
132 (set-face-attribute 'fixed-pitch nil
133 :family font-family-mono
134 :weight 'medium
135 :height font-height)
136 (set-face-attribute 'variable-pitch nil
137 :family font-family-sans
138 :weight 'regular)
139
140 (when (fboundp 'set-fontset-font)
141 (set-fontset-font t 'symbol "Apple Color Emoji")
142 (set-fontset-font t 'symbol "Noto Color Emoji" nil 'append)
143 (set-fontset-font t 'symbol "Segoe UI Emoji" nil 'append)
144 (set-fontset-font t 'symbol "Symbola" nil 'append))
145
146 (require 'modus-themes)
147 (defvar modus-themes-preset-overrides-cooler)
148 (setopt modus-themes-common-palette-overrides
149 `((border-mode-line-active unspecified)
150 (border-mode-line-inactive unspecified)
151 ,@modus-themes-preset-overrides-cooler)
152 modus-themes-to-rotate '(modus-operandi modus-vivendi)
153 modus-themes-mixed-fonts t
154 modus-themes-headings '((0 . (variable-pitch semilight 1.5))
155 (1 . (regular 1.4))
156 (2 . (regular 1.3))
157 (3 . (regular 1.2))
158 (agenda-structure . (variable-pitch light 2.2))
159 (agenda-date . (variable-pitch regular 1.3))
160 (t . (regular 1.15))))
161
162 ;; Color scheme management - sync with system dconf settings
163 (defvar vde/themes-plist
164 '(:dark modus-vivendi :light modus-operandi)
165 "Themes to use for dark and light modes.")
166
167 (defun vde/color-scheme-get-system ()
168 "Get current system color scheme from dconf."
169 (when (eq system-type 'gnu/linux)
170 (let* ((raw-scheme (shell-command-to-string
171 "/run/current-system/sw/bin/dconf read /org/gnome/desktop/interface/color-scheme"))
172 (scheme (string-trim raw-scheme)))
173 (if (string-match-p "prefer-dark" scheme)
174 :dark
175 :light))))
176
177 (defun vde/color-scheme-set-emacs (scheme)
178 "Set Emacs theme based on SCHEME (:dark or :light)."
179 (let ((theme (plist-get vde/themes-plist scheme)))
180 (when theme
181 (mapc #'disable-theme custom-enabled-themes)
182 (load-theme theme :no-confirm))))
183
184 (defun vde/color-scheme-sync ()
185 "Sync Emacs theme with system color scheme."
186 (interactive)
187 (let ((scheme (vde/color-scheme-get-system)))
188 (when scheme
189 (vde/color-scheme-set-emacs scheme))))
190
191 (defun vde/color-scheme-toggle ()
192 "Toggle system color scheme and sync Emacs."
193 (interactive)
194 (when (eq system-type 'gnu/linux)
195 (start-process "toggle-color-scheme" nil "toggle-color-scheme")
196 ;; Wait a bit for the system to update, then sync
197 (run-with-timer 0.5 nil #'vde/color-scheme-sync)))
198
199 (declare-function vde/color-scheme-sync "init")
200 (declare-function vde/color-scheme-get-system "init")
201 (declare-function vde/color-scheme-set-emacs "init")
202
203 ;; Initial theme setup
204 (if (display-graphic-p)
205 (vde/color-scheme-sync)
206 (load-theme 'modus-vivendi :no-confirm))
207
208 ;; Watch for dbus signals when color-scheme changes
209 (when (and (eq system-type 'gnu/linux)
210 (require 'dbus nil t))
211 (declare-function dbus-register-signal "dbus")
212 (dbus-register-signal
213 :session
214 "ca.desrt.dconf"
215 "/ca/desrt/dconf/Writer/user"
216 "ca.desrt.dconf.Writer"
217 "Notify"
218 (lambda (&rest args)
219 (when (string-match-p "color-scheme" (format "%s" args))
220 (vde/color-scheme-sync)))))
221
222 ;; Keybinding for toggling color scheme
223 (declare-function vde/color-scheme-toggle "init")
224 (global-set-key (kbd "C-c t t") #'vde/color-scheme-toggle))
225
226(setopt load-prefer-newer t) ; Always load newer compiled files
227(setopt ad-redefinition-action 'accept) ; Silence advice redefinition warnings
228;; (setopt debug-on-error t)
229(setopt byte-compile-debug t)
230
231;; Configure `use-package' prior to loading it.
232(eval-and-compile
233 (setq use-package-always-ensure nil)
234 (setq use-package-always-defer nil)
235 (setq use-package-always-demand nil)
236 (setq use-package-expand-minimally nil)
237 (setq use-package-enable-imenu-support t)
238 (setq use-package-compute-statistics t))
239
240(eval-when-compile
241 (require 'use-package))
242
243(require 'info) ;; XXX ensure the var exists even before loading `info.el'.
244
245;; Add site-lisp to load-path for local elisp files
246(add-to-list 'load-path (expand-file-name "site-lisp" user-emacs-directory))
247
248;; TODO: Re-enable init-func when needed for custom macros
249;; (use-package init-func)
250;; ;; TODO: do useful stuff with the macro instead
251;; (vde/run-and-delete-frame my-greet-and-close ()
252;; "Displays a greeting and closes the frame after a short delay."
253;; (message "Hello from a macro-defined function! Closing soon...")
254;; (sit-for 2))
255
256(use-package emacs
257 :bind
258 ("C-x m" . mark-defun)
259 ("C-x C-b" . bs-show)
260 ("M-o" . other-window)
261 ("M-j" . duplicate-dwim)
262 ("<f5>" . revert-buffer)
263 ;; (:map completion-preview-active-mode-map
264 ;; ("M-n" . #'completion-preview-next-candidate)
265 ;; ("M-p" . #'completion-preview-prev-candidate))
266 :custom
267 (create-lockfiles nil)
268 ;; Versioned backups in central directory
269 (make-backup-files t)
270 (version-control t) ; Numbered backups
271 (kept-new-versions 10) ; Keep 10 newest
272 (kept-old-versions 2) ; Keep 2 oldest
273 (delete-old-versions t) ; Auto-delete excess
274 (backup-by-copying t) ; Don't clobber symlinks
275 (backup-directory-alist '(("." . "~/.local/share/emacs/backups/")))
276 ;; Auto-save files in central directory
277 (auto-save-file-name-transforms '((".*" "~/.local/share/emacs/auto-saves/" t)))
278 (use-short-answers t "Use short answer y/n")
279 ;; (tab-always-indent 'complete)
280 ;; (tab-first-completion 'word-or-paren-or-punct)
281 (enable-local-variables :all)
282 ;; (select-enable-clipboard t)
283 ;; (select-enable-primary t)
284 (comment-multi-line t)
285 (read-extended-command-predicate #'command-completion-default-include-p)
286 (mouse-autoselect 1)
287 (completion-cycle-threshold 2)
288 (completion-ignore-case t)
289 (completion-show-inline-help nil)
290 (completions-detailed t)
291 (enable-recursive-minibuffers t)
292 (read-buffer-completion-ignore-case t)
293 (read-file-name-completion-ignore-case t)
294 (find-ls-option '("-exec ls -ldh {} +" . "-ldh")) ; find-dired results with human readable sizes
295 (global-auto-revert-non-file-buffers t "Auto revert non-file buffers")
296 (switch-to-buffer-obey-display-actions t "Don't distuingish automatic and manual window switching")
297 (window-combination-resize t "Resize window proportionally")
298 (isearch-lazy-count t "Show size of search results")
299 (lazy-count-prefix-format "(%s/%s) " "Format of search results")
300 (lazy-highlight-initial-delay 0 "No delay before highlight search matches")
301 (isearch-allow-scroll t "Allow scrolling while searching")
302 (isearch-allow-motion t "Allow movement commands while searching")
303 :hook
304 (after-init . global-hl-line-mode)
305 (after-init . global-completion-preview-mode)
306 (after-init . auto-insert-mode)
307 (after-init . pixel-scroll-mode)
308 :config
309 (with-current-buffer "*Messages*" (emacs-lock-mode 'kill))
310 (with-current-buffer "*scratch*" (emacs-lock-mode 'kill))
311 (display-time-mode -1)
312 (when (fboundp 'tooltip-mode) (tooltip-mode -1))
313 (when (fboundp 'blink-cursor-mode) (blink-cursor-mode -1))
314 (setenv "GIT_EDITOR" (format "emacs --init-dir=%s " (shell-quote-argument user-emacs-directory)))
315 (setenv "EDITOR" (format "emacs --init-dir=%s " (shell-quote-argument user-emacs-directory)))
316 (delete-selection-mode 1)
317 (declare-function minibuffer-keyboard-quit "delsel")
318 (defun er-keyboard-quit ()
319 "Smater version of the built-in `keyboard-quit'.
320
321The generic `keyboard-quit' does not do the expected thing when
322the minibuffer is open. Whereas we want it to close the
323minibuffer, even without explicitly focusing it."
324 (interactive)
325 (if (active-minibuffer-window)
326 (if (minibufferp)
327 (minibuffer-keyboard-quit)
328 (abort-recursive-edit))
329 (keyboard-quit)))
330 (declare-function er-keyboard-quit "init")
331 (global-set-key [remap keyboard-quit] #'er-keyboard-quit))
332
333(use-package view
334 :commands (view-mode)
335 :init
336 (declare-function View-scroll-page-backward "view")
337 (declare-function View-scroll-page-forward "view")
338 :custom
339 (view-read-only t "Enable view-mode when entering read-only")
340 :bind (("C-<escape>" . view-mode)
341 :map view-mode-map
342 ;; Navigation
343 ("n" . next-line)
344 ("p" . previous-line)
345 ("f" . forward-char)
346 ("b" . backward-char)
347
348 ;; Beginning/end of line
349 ("a" . beginning-of-line)
350 ("e" . end-of-line)
351
352 ;; Quick exit to edit mode
353 ("i" . View-exit)
354 ("j" . next-line)
355 ("k" . previous-line)
356 ("h" . backward-char)
357 ("l" . forward-char)
358
359 ;; Page movement
360 ("u" . (lambda()
361 (interactive)
362 (View-scroll-page-backward 3)))
363 ("d" . (lambda()
364 (interactive)
365 (View-scroll-page-forward 3)))
366
367 ;; Beginning/end of line (Vim style)
368 ("0" . beginning-of-line)
369 ("$" . end-of-line)
370
371 ;; Beginning/end of buffers
372 ("g" . beginning-of-buffer)
373 ("G" . end-of-buffer)
374
375 ;; Other bespoke bindings
376 (";" . other-window)
377
378 ("SPC" . nil))
379 :hook
380 ;; Visual feedback - box cursor in view mode, bar when editing
381 (view-mode . view-mode-hookee+)
382 :config
383 (defun view-mode-hookee+ ()
384 (setq cursor-type (if view-mode 'box 'bar))))
385
386(use-package tramp
387 :custom
388 ;; Tramp
389 (remote-file-name-inhibit-locks t "No lock files on remote files")
390 (tramp-use-scp-direct-remote-copying t "Use direct copying between two remote hosts")
391 (remote-file-name-inhibit-auto-save-visited t "Do not auto-save remote files")
392 (tramp-copy-size-limit (* 1024 1024)) ;; 1MB
393 (tramp-verbose 2)
394 :config
395 (declare-function tramp-compile-disable-ssh-controlmaster-options "tramp")
396 ;; Add custom SSH options to disable YubiKey agent for TRAMP connections
397 (with-eval-after-load 'tramp-sh
398 (defvar tramp-use-ssh-controlmaster-options)
399 (setq tramp-use-ssh-controlmaster-options nil)
400 (let ((ssh-method (assoc "ssh" tramp-methods)))
401 (when ssh-method
402 (let* ((login-args (cadr (assq 'tramp-login-args (cdr ssh-method))))
403 (new-login-args (append login-args
404 '(("-o" "IdentitiesOnly=yes")
405 ("-o" "IdentityAgent=none")))))
406 (setf (cadr (assq 'tramp-login-args (cdr ssh-method))) new-login-args)))))
407 (connection-local-set-profile-variables
408 'remote-direct-async-process
409 '((tramp-direct-async-process . t)))
410
411 (connection-local-set-profiles
412 '(:application tramp :protocol "scp")
413 'remote-direct-async-process)
414
415 (defvar magit-tramp-pipe-stty-settings)
416 (with-eval-after-load 'magit
417 (setq magit-tramp-pipe-stty-settings 'pty))
418 (with-eval-after-load 'tramp
419 (with-eval-after-load 'compile
420 (remove-hook 'compilation-mode-hook #'tramp-compile-disable-ssh-controlmaster-options)))
421
422 (add-to-list 'tramp-remote-path 'tramp-own-remote-path)
423 (add-to-list 'tramp-remote-path "/home/vincent/.local/state/nix/profile/bin/")
424 (add-to-list 'tramp-remote-path "~/bin/")
425 ;; (add-to-list 'tramp-connection-properties
426 ;; (list (regexp-quote "/ssh:aomi.home:")
427 ;; "remote-shell" "/home/vincent/.local/state/nix/profile/bin/zsh"))
428 )
429
430(use-package passage
431 :commands (passage-get))
432
433(use-package find-file
434 :bind ("C-x C-g" . ff-find-other-file))
435
436(use-package icomplete
437 :unless noninteractive
438 :hook
439 (icomplete-minibuffer-setup
440 . (lambda()(interactive)
441 (setq-local completion-styles '(flex partial-completion initials basic))))
442 (after-init . fido-vertical-mode)
443 :custom
444 (icomplete-compute-delay 0.01)
445 :config
446 ;; Unbind C-, from icomplete to allow embark-act to work
447 (define-key icomplete-minibuffer-map (kbd "C-,") nil)
448 (define-key icomplete-fido-mode-map (kbd "C-,") nil))
449
450(use-package display-line-numbers
451 :unless noninteractive
452 :hook (prog-mode . display-line-numbers-mode)
453 :config
454 (setq-default display-line-numbers-type 'relative)
455 (defun vde/toggle-line-numbers ()
456 "Toggles the display of line numbers. Applies to all buffers."
457 (interactive)
458 (if (bound-and-true-p display-line-numbers-mode)
459 (display-line-numbers-mode -1)
460 (display-line-numbers-mode)))
461 :bind ("<f7>" . vde/toggle-line-numbers))
462
463(use-package kkp
464 :if (not (display-graphic-p))
465 :hook (after-init . global-kkp-mode))
466
467(use-package helpful
468 :unless noninteractive
469 :bind (("C-h f" . helpful-callable)
470 ("C-h F" . helpful-function)
471 ("C-h M" . helpful-macro)
472 ("C-c h S" . helpful-at-point)
473 ("C-h k" . helpful-key)
474 ("C-h v" . helpful-variable)
475 ("C-h C" . helpful-command)))
476
477(use-package flymake
478 :bind
479 ("C-c f b" . flymake-show-buffer-diagnostics)
480 (:map flymake-mode-map
481 ("M-n" . flymake-goto-next-error)
482 ("M-p" . flymake-goto-prev-error))
483 :hook
484 (prog-mode . flymake-mode))
485
486(use-package treesit-fold
487 :hook
488 (after-init . global-treesit-fold-mode)
489 :custom
490 (treesit-fold-line-count-show t) ; Show line count in folded regions
491 (treesit-fold-line-count-format " <%d lines> ")
492 :config
493 (global-set-key (kbd "C-c f f") 'treesit-fold-close)
494 (global-set-key (kbd "C-c f o") 'treesit-fold-open))
495
496(use-package scopeline
497 :hook prog-mode
498 :custom-face
499 (scopeline-face ((t (:height 0.8 :inherit shadow)))))
500
501(use-package aggressive-indent
502 :commands (aggressive-indent-mode)
503 :hook
504 (emacs-lisp-mode . aggressive-indent-mode))
505
506(use-package save-place
507 :defer 1
508 :config (save-place-mode 1))
509
510(use-package symbol-overlay
511 :custom
512 (symbol-overlay-idle-time 0.2)
513 :bind
514 ("M-s s i" . symbol-overlay-put)
515 ("M-N" . symbol-overlay-jump-next)
516 ("M-P" . symbol-overlay-jump-prev)
517 ("M-s s r" . symbol-overlay-rename)
518 ("M-s s c" . symbol-overlay-remove-all)
519 :hook
520 (prog-mode . symbol-overlay-mode))
521
522(use-package savehist
523 :unless noninteractive
524 :hook (after-init . savehist-mode)
525 :custom
526 (history-length 10000)
527 (savehist-save-minibuffer-history t)
528 (savehist-delete-duplicates t)
529 (savehist-autosave-interval 180)
530 (savehist-additional-variables '(extended-command-history
531 search-ring
532 regexp-search-ring
533 comint-input-ring
534 compile-history
535 last-kbd-macro
536 shell-command-history)))
537
538(use-package newcomment
539 :unless noninteractive
540 :custom
541 (comment-empty-lines t)
542 (comment-fill-column nil)
543 (comment-multi-line t)
544 (comment-style 'multi-line)
545 :config
546 (defun prot/comment-dwim (&optional arg)
547 "Alternative to `comment-dwim': offers a simple wrapper
548 around `comment-line' and `comment-dwim'.
549
550 If the region is active, then toggle the comment status of the
551 region or, if the major mode defines as much, of all the lines
552 implied by the region boundaries.
553
554 Else toggle the comment status of the line at point."
555 (interactive "*P")
556 (if (use-region-p)
557 (comment-dwim arg)
558 (save-excursion
559 (comment-line arg))))
560 :bind (("C-;" . prot/comment-dwim)
561 ("C-:" . comment-kill)
562 ("M-;" . comment-indent)
563 ("C-x C-;" . comment-box)))
564
565(use-package dired
566 :custom
567 (dired-hide-details-hide-information-lines 'nil)
568 (dired-kill-when-opening-new-dired-buffer 't)
569 (dired-dwim-target t)
570 :bind
571 (:map dired-mode-map
572 ("E" . wdired-change-to-wdired-mode)
573 ("l" . dired-find-file))
574 :custom
575 (dired-vc-rename-file t)
576 :hook
577 (dired-mode . dired-omit-mode)
578 (dired-mode . dired-hide-details-mode)
579 ;; (dired-mode . dired-sort-toggle-or-edit) ; I don't like the "default by date" behavior
580 )
581
582(use-package alert
583 :defer 2
584 :init
585 (declare-function alert "alert")
586 (defun alert-after-finish-in-background (buf str)
587 (when (or (not (get-buffer-window buf 'visible)) (not (frame-focus-state)))
588 (alert str :buffer buf)))
589 :config
590 (defvar alert-default-style)
591 (setq alert-default-style 'libnotify))
592
593(use-package elec-pair
594 :hook (after-init-hook . electric-pair-mode))
595
596(use-package uniquify
597 :custom
598 (uniquify-buffer-name-style 'forward)
599 (uniquify-strip-common-suffix t)
600 (uniquify-after-kill-buffer-p t))
601
602(use-package compile
603 :unless noninteractive
604 :commands (compile)
605 :custom
606 (compilation-always-kill t)
607 (compilation-scroll-output t)
608 (ansi-color-for-compilation-mode t)
609 :config
610 (declare-function alert-after-finish-in-background "init")
611 (add-hook 'compilation-finish-functions #'alert-after-finish-in-background))
612
613(use-package subword
614 :diminish
615 :hook (prog-mode-hook . subword-mode))
616
617;; Recentf
618(use-package recentf
619 :defer t
620 :hook
621 (after-nit . recentf-mode)
622 :bind (("C-x C-r" . recentf-open)))
623
624(use-package prog-mode
625 :hook
626 (prog-mode . eldoc-mode)
627 :custom
628 (eldoc-idle-delay 0.2))
629
630(use-package eglot
631 :bind
632 (:map eglot-mode-map
633 ("C-c e a" . eglot-code-actions)
634 ("C-c e r" . eglot-reconnect)
635 ("<f2>" . eglot-rename)
636 ("C-c e ?" . eldoc-print-current-symbol-info))
637 :custom
638 (eglot-autoshutdown t)
639 (eglot-confirm-server-initiated-edits nil)
640 :config
641 (declare-function eglot-format-buffer "eglot")
642 (declare-function eglot-managed-p "eglot")
643 (add-to-list 'eglot-ignored-server-capabilities :documentHighlightProvider)
644 (add-to-list 'eglot-server-programs `(json-mode "vscode-json-language-server" "--stdio"))
645 (add-to-list 'eglot-server-programs '(nix-mode . ("nil")))
646 ;; (add-to-list 'eglot-server-programs
647 ;; '(go-mode . ("harper-ls" "--stdio")))
648 ;; (add-to-list 'eglot-server-programs
649 ;; '(text-mode . ("harper-ls" "--stdio")))
650 ;; (add-to-list 'eglot-server-programs
651 ;; '(org-mode . ("harper-ls" "--stdio")))
652 (add-to-list 'eglot-server-programs
653 '(markdown-mode . ("harper-ls" "--stdio")))
654 (setq-default eglot-workspace-configuration
655 '(
656 :gopls (
657 :usePlaceholders t
658 ;; See https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md
659 :analyses (
660 :QF1006 t
661 :QF1007 t
662 :S1002 t
663 :S1005 t
664 :S1006 t
665 :S1008 t
666 :S1025 t
667 :SA1003 t
668 :SA1014 t
669 :SA1015 t
670 :SA1023 t
671 :SA1032 t
672 :SA2002 t
673 :SA4023 t
674 :SA4031 t
675 :SA5000 t
676 :SA5010 t
677 :SA5000 t
678 :SA6000 t
679 :SA6001 t
680 :SA6002 t
681 :SA6003 t
682 :SA9003 t
683 :SA9007 t
684 :ST1000 t
685 :ST1001 t
686 :ST1005 t
687 :ST1013 t
688 :ST1015 t
689 :ST1016 t
690 :ST1017 t
691 :ST1019 t
692 :ST1020 t
693 :ST1021 t
694 :ST1022 t
695 :ST1023 t
696 :shadow t
697 )
698 ;; See https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md
699 :hints (:constantValues t :compositeLiteralTypes t :compositeLiteralFields t))
700 :nil (
701 :formatting (:command ["nixfmt"])
702 :nix (
703 :maxMemoryMB 2560
704 :autoEvalInputs t
705 :nixpkgsInputName "nixpkgs"
706 )
707 )
708 :pylsp (
709 :configurationSources ["flake8"]
710 :plugins (:pycodestyle (:enabled nil)
711 :black (:enabled t)
712 :mccabe (:enabled nil)
713 :flake8 (:enabled t)))))
714 (defun eglot-format-buffer-on-save ()
715 (if (and (project-current) (eglot-managed-p))
716 (add-hook 'before-save-hook #'eglot-format-buffer nil 'local)
717 (remove-hook 'before-save-hook #'eglot-format-buffer 'local)))
718 (declare-function eglot-format-buffer-on-save "init")
719 (add-hook 'eglot-managed-mode-hook #'eglot-format-buffer-on-save)
720
721 ;; Don't auto-start eglot on TRAMP files (too slow/unreliable)
722 (defun eglot-ensure-unless-tramp ()
723 "Start eglot unless we're on a TRAMP remote file."
724 (unless (file-remote-p default-directory)
725 (eglot-ensure)))
726 (declare-function eglot-ensure-unless-tramp "init")
727
728 :hook
729 ;; (before-save . gofmt-before-save)
730 ;; (before-save . eglot-format-buffer)
731 (nix-mode . eglot-ensure-unless-tramp)
732 (nix-ts-mode . eglot-ensure-unless-tramp)
733 (rust-mode . eglot-ensure-unless-tramp)
734 (rust-ts-mode . eglot-ensure-unless-tramp)
735 (python-mode . eglot-ensure-unless-tramp)
736 (python-ts-mode . eglot-ensure-unless-tramp)
737 (go-mode . eglot-ensure-unless-tramp)
738 (go-ts-mode . eglot-ensure-unless-tramp)
739 (sh-mode . eglot-ensure-unless-tramp)
740 (sh-script-mode . eglot-ensure-unless-tramp)
741 (org-mode . eglot-ensure-unless-tramp)
742 (markdown-mode . eglot-ensure-unless-tramp))
743
744(setq major-mode-remap-alist
745 '((python-mode . python-ts-mode)
746 (go-mode . go-ts-mode)))
747
748(use-package markdown-mode
749 :mode (("README\\.md\\'" . gfm-mode)
750 ("\\.md\\'" . markdown-mode)
751 ("\\.markdown\\'" . markdown-mode))
752 :hook ((markdown-mode . visual-line-mode)
753 (gfm-mode . visual-line-mode)))
754
755(use-package yaml-ts-mode
756 :mode "\\.ya?ml\\'"
757 :hook ((yaml-ts-mode . display-line-numbers-mode)
758 (yaml-ts-mode . outline-minor-mode)
759 (yaml-ts-mode . electric-pair-local-mode))
760 :config
761 (setq-local outline-regexp "^ *\\([A-Za-z0-9_-]*: *[>|]?$\\|-\\b\\)")
762 (font-lock-add-keywords
763 'yaml-ts-mode
764 '(("\\($(\\(workspaces\\|context\\|params\\)\.[^)]+)\\)" 1 'font-lock-constant-face prepend)
765 ("kind:\s*\\(.*\\)\n" 1 'font-lock-keyword-face prepend))))
766
767(use-package orgalist
768 :commands (orgalist-mode)
769 :hook ((markdown-mode . orgalist-mode)
770 (gfm-mode . orgalist-mode)))
771
772(use-package dockerfile-ts-mode
773 :mode (("Dockerfile\\'" . dockerfile-ts-mode)
774 ("\\.Dockerfile\\'" . dockerfile-ts-mode-map)
775 ("Containerfile\\'" . dockerfile-ts-mode)
776 ("\\.Containerfile\\'" . dockerfile-ts-mode-map)))
777
778(use-package go-ts-mode
779 :mode (("\\.go$" . go-ts-mode)
780 ("\\.go" . go-ts-mode)
781 ("\\.go\\'" . go-ts-mode))
782 :hook ((go-ts-mode . vde/go-mode-setup))
783 :config
784 (defun vde/go-mode-setup ()
785 "Setup for go-mode."
786 (setq-local ff-other-file-alist
787 '(("_test\\.go\\'" (".go"))
788 ("\\.go\\'" ("_test.go"))
789 ))))
790
791(use-package lua-ts-mode
792 :mode "\\.lua\\'"
793 :hook (lua-ts-mode . eglot-ensure-unless-tramp))
794
795(use-package nix-ts-mode
796 :if (executable-find "nix")
797 :mode ("\\.nix\\'" "\\.nix.in\\'"))
798
799(use-package terraform-ts-mode
800 :if (executable-find "terraform-ls")
801 :mode ("\\.tf\\'" "\\.tfvars\\'"))
802
803(use-package nix-drv-mode
804 :if (executable-find "nix")
805 :after nix-mode
806 :mode "\\.drv\\'")
807
808(use-package nix-shell
809 :if (executable-find "nix")
810 :after nix-mode
811 :commands (nix-shell-unpack nix-shell-configure nix-shell-build))
812
813(use-package nixpkgs-fmt
814 :if (executable-find "nix")
815 :after nix-ts-mode
816 :custom
817 (nixpkgs-fmt-command "nixfmt")
818 :config
819 (add-hook 'nix-ts-mode-hook 'nixpkgs-fmt-on-save-mode))
820
821(use-package minions
822 :hook (after-init . minions-mode)
823 :config
824 (defvar minions-prominent-modes)
825 (add-to-list 'minions-prominent-modes 'flymake-mode))
826
827(use-package vundo
828 :bind (("M-u" . undo)
829 ("M-U" . undo-redo)
830 ("C-x u" . vundo)))
831
832(use-package vde-vcs
833 :commands (vde/gh-get-current-repo vde/vc-browse-remote)
834 :bind (("C-x v B" . vde/vc-browse-remote)))
835
836(use-package project-func
837 :commands (vde/project-magit-status vde/project-eat vde/project-vterm vde/project-run-in-vterm vde/project-try-local vde/open-readme))
838
839(use-package project
840 :commands (project-find-file project-find-regexp)
841 :custom
842 (project-switch-commands '((?f "File" project-find-file)
843 (?g "Grep" project-find-regexp)
844 (?d "Dired" project-dired)
845 (?b "Buffer" project-switch-to-buffer)
846 (?q "Query replace" project-query-replace-regexp)
847 (?m "Magit" vde/project-magit-status)
848 (?e "Eshell" project-eshell)
849 (?E "Eat" vde/project-eat)
850 (?s "Vterm" vde/project-vterm)
851 (?R "README" vde/open-readme)
852 (?g "Checkout GitHub PR" checkout-github-pr)))
853 (project-mode-line t)
854 (project-compilation-buffer-name-function 'project-prefixed-buffer-name)
855 ;; (project-vc-extra-root-markers '(".project" "Cargo.toml" "pyproject.toml" "requirements.txt" "go.mod"))
856 :bind
857 ("C-x p v" . vde/project-magit-status)
858 ("C-x p s" . vde/project-vterm)
859 ("C-x p X" . vde/project-run-in-vterm)
860 ("C-x p E" . vde/project-eat)
861 ("C-x p G" . checkout-github-pr)
862 ("C-x p F" . flymake-show-project-diagnostics)
863 ("C-x p R" . vde/project-refresh-list)
864 :config
865 (setq project-list-exclude
866 '("/build\\(/\\|\\'\\)" "/node_modules\\(/\\|\\'\\)"
867 "/.cache\\(/\\|\\'\\)" "/target\\(/\\|\\'\\)" "/.direnv\\(/\\|\\'\\)"))
868
869 (defun vde/project-refresh-list ()
870 "Rescan ~/src for projects."
871 (interactive)
872 (setq project--list nil) ; Clear instead of checking each zombie
873 (project-remember-projects-under "~/src" t)
874 (message "Project list refreshed"))
875
876 ;; Populate project list on first run if empty
877 (unless project--list
878 (project-remember-projects-under "~/src" t)))
879
880(use-package eshell
881 :commands (eshell eshell-here)
882 :config
883 (defvar eshell-last-dir-ring)
884 (defvar consult-dir-sources)
885 (declare-function eshell-find-previous-directory "eshell")
886 (declare-function consult-dir--pick "consult-dir")
887 (declare-function ring-elements "ring")
888 (declare-function eshell/cd "eshell")
889 (add-to-list 'eshell-modules-list 'eshell-rebind)
890
891 (defun eshell-here ()
892 "Open eshell in the directory of the current buffer's file."
893 (interactive)
894 (let* ((parent (if (buffer-file-name)
895 (file-name-directory (buffer-file-name))
896 default-directory))
897 (name (car (last (split-string parent "/" t)))))
898 (eshell "new")
899 (rename-buffer (concat "*eshell: " name "*"))))
900
901 ;; Handy aliases
902 (defalias 'ff 'find-file)
903 (defalias 'e 'find-file)
904 (defalias 'd 'dired)
905
906 (defun eshell/cdg ()
907 "Change directory to the project's root."
908 (eshell/cd (locate-dominating-file default-directory ".git")))
909
910 (defun eshell/j (&optional regexp)
911 "Navigate to a previously visited directory in eshell."
912 (let ((eshell-dirs (delete-dups
913 (mapcar 'abbreviate-file-name
914 (ring-elements eshell-last-dir-ring)))))
915 (cond
916 ((and (not regexp) (featurep 'consult-dir))
917 (let* ((consult-dir--source-eshell `(:name "Eshell"
918 :narrow ?e
919 :category file
920 :face consult-file
921 :items ,eshell-dirs))
922 (consult-dir-sources (cons consult-dir--source-eshell
923 consult-dir-sources)))
924 (eshell/cd (substring-no-properties
925 (consult-dir--pick "Switch directory: ")))))
926 (t (eshell/cd (if regexp (eshell-find-previous-directory regexp)
927 (completing-read "cd: " eshell-dirs)))))))
928
929 ;; Use system su/sudo instead of eshell builtins
930 (with-eval-after-load "em-unix"
931 (unintern 'eshell/su nil)
932 (unintern 'eshell/sudo nil))
933
934 :hook (eshell-mode . with-editor-export-editor))
935
936(use-package eat
937 :commands (eat)
938 :init
939 (defvar eat-kill-buffer-on-exit)
940 (defvar eat-enable-yank-to-terminal)
941 (setq-default explicit-shell-file-name "zsh"
942 shell-file-name "zsh")
943 (setq eat-kill-buffer-on-exit t
944 eat-enable-yank-to-terminal t)
945 :hook ((eshell-mode . eat-eshell-mode)
946 (eshell-mode . eat-eshell-visual-command-mode)))
947
948(use-package vterm
949 :commands (vterm)
950 :custom
951 (vterm-kill-buffer-on-exit t)
952 (vterm-max-scrollback 100000))
953
954;; TODO adapt this to my needs
955;; (defun tkj/vc-git-grep-current-line ()
956;; "Search Git project for the current line."
957;; (interactive)
958;; (let ((current-line (string-trim (thing-at-point 'line t))))
959;; (if (string-empty-p current-line)
960;; (message "No line in sight")
961;; (vc-git-grep current-line "*" (vc-git-root default-directory)))))
962;; (global-set-key (kbd "C-c p l") 'tkj/vc-git-grep-current-line)
963;;
964;; (defun tkj/vc-git-grep-symbol ()
965;; "Search Git project for the symbol at point. This could be any word or
966;; programming symbol, like a function or variable."
967;; (interactive)
968;; (let ((symbol (string-trim (thing-at-point 'symbol t))))
969;; (if (string-empty-p symbol)
970;; (message "You point at nothing")
971;; (vc-git-grep symbol "*" (vc-git-root default-directory)))))
972;; (global-set-key (kbd "C-c p s") 'tkj/vc-git-grep-symbol)
973
974(use-package vc-dir
975 :bind (("C-x v D" . project-vc-dir)
976 :map vc-dir-mode-map
977 ("=" . vc-diff)
978 ("v" . vc-next-action)
979 ("C-c C-c" . vc-next-action)))
980
981(use-package magit
982 :unless noninteractive
983 :commands (magit-status magit-clone magit-pull magit-blame magit-log-buffer-file magit-log magit-file-dispatch)
984 :bind (("C-c v c" . magit-commit)
985 ("C-c v C" . magit-checkout)
986 ("C-c v b" . magit-branch)
987 ("C-c v d" . magit-dispatch)
988 ("C-c v f" . magit-fetch)
989 ("C-c v g" . magit-blame)
990 ("C-c v l" . magit-log-buffer-file)
991 ("C-c v L" . magit-log)
992 ("C-c v p" . magit-pull)
993 ("C-c v P" . magit-push)
994 ("C-c v r" . magit-rebase)
995 ("C-c v s" . magit-stage)
996 ("C-c v v" . magit-status)
997 )
998 :custom
999 (magit-save-repository-buffers 'dontask)
1000 (magit-refs-show-commit-count 'all)
1001 (magit-branch-prefer-remote-upstream '("main"))
1002 (magit-display-buffer-function #'magit-display-buffer-fullframe-status-v1)
1003 (magit-bury-buffer-function #'magit-restore-window-configuration)
1004 (magit-refresh-status-buffer nil "don't automatically refresh the status buffer after running a git command")
1005 (magit-commit-show-diff nil "don't show the diff by default in the commit buffer. Use `C-c C-d' to display it")
1006 (magit-branch-direct-configure nil "don't show git variables in magit branch")
1007 (magit-diff-visit-prefer-worktree t "prefer visiting the worktree file")
1008 :config
1009 (declare-function magit-insert-worktrees "magit")
1010 (add-hook 'magit-status-sections-hook #'magit-insert-worktrees t)
1011 ;; cargo-culted from https://github.com/magit/magit/issues/3717#issuecomment-734798341
1012 ;; valid gitlab options are defined in https://docs.gitlab.com/ee/user/project/push_options.html
1013 ;;
1014 ;; the second argument to transient-append-suffix is where to append
1015 ;; to, not sure what -u is, but this works
1016 (transient-append-suffix 'magit-push "-u"
1017 '(1 "=s" "Skip gitlab pipeline" "--push-option=ci.skip"))
1018 (transient-append-suffix 'magit-push "=s"
1019 '(1 "=m" "Create gitlab merge-request" "--push-option=merge_request.create"))
1020 (transient-append-suffix 'magit-push "=m"
1021 '(1 "=o" "Set push option" "--push-option=")) ;; Will prompt, can only set one extra
1022 )
1023
1024(use-package git-commit
1025 :init
1026 (defvar git-commit-mode-map)
1027 :bind (:map git-commit-mode-map
1028 ("C-c C-h" . git-commit-co-authored)))
1029
1030(use-package ediff
1031 :commands (ediff ediff-files ediff-merge ediff3 ediff-files3 ediff-merge3)
1032 :custom
1033 (ediff-window-setup-function 'ediff-setup-windows-plain)
1034 (ediff-split-window-function 'split-window-horizontally)
1035 (ediff-diff-options "-w")
1036 :hook
1037 (ediff-after-quit-hook-internal . winner-undo))
1038
1039(use-package diff
1040 :custom
1041 (diff-default-read-only nil)
1042 (diff-advance-after-apply-hunk t)
1043 (diff-update-on-the-fly t)
1044 (diff-refine 'font-lock)
1045 (diff-font-lock-prettify nil)
1046 (diff-font-lock-syntax nil))
1047
1048(use-package gitconfig-mode
1049 :commands (gitconfig-mode)
1050 :mode (("/\\.gitconfig\\'" . gitconfig-mode)
1051 ("/\\.git/config\\'" . gitconfig-mode)
1052 ("/git/config\\'" . gitconfig-mode)
1053 ("/\\.gitmodules\\'" . gitconfig-mode)))
1054
1055(use-package gitignore-mode
1056 :commands (gitignore-mode)
1057 :mode (("/\\.gitignore\\'" . gitignore-mode)
1058 ("/\\.git/info/exclude\\'" . gitignore-mode)
1059 ("/git/ignore\\'" . gitignore-mode)))
1060
1061(use-package gitattributes-mode
1062 :commands (gitattributes-mode)
1063 :mode (("/\\.gitattributes" . gitattributes-mode)))
1064
1065(use-package diff-hl
1066 :init
1067 (defvar diff-hl-command-map)
1068 :hook (find-file . diff-hl-mode)
1069 :hook (prog-mode . diff-hl-mode)
1070 :hook (magit-post-refresh . diff-hl-magit-post-refresh)
1071 :bind
1072 (:map diff-hl-command-map
1073 ("n" . diff-hl-next-hunk)
1074 ("p" . diff-hl-previous-hunk)
1075 ("[" . nil)
1076 ("]" . nil)
1077 ("DEL" . diff-hl-revert-hunk)
1078 ("<delete>" . diff-hl-revert-hunk)
1079 ("SPC" . diff-hl-mark-hunk)
1080 :map vc-prefix-map
1081 ("n" . diff-hl-next-hunk)
1082 ("p" . diff-hl-previous-hunk)
1083 ("s" . diff-hl-stage-dwim)
1084 ("DEL" . diff-hl-revert-hunk)
1085 ("<delete>" . diff-hl-revert-hunk)
1086 ("SPC" . diff-hl-mark-hunk))
1087 :config
1088 (put 'diff-hl-inline-popup-hide
1089 'repeat-map 'diff-hl-command-map))
1090
1091(use-package diff-hl-show-hunk
1092 :after (diff-hl))
1093
1094(use-package diff-hl-dired
1095 :after (diff-hl)
1096 :hook (dired-mode . diff-hl-dired-mode))
1097
1098(use-package corfu
1099 :init
1100 (defvar corfu-map)
1101 :custom
1102 (corfu-auto 't)
1103 (corfu-auto-delay 1)
1104 (corfu-cycle 't)
1105 (corfu-preselect 'prompt)
1106 :bind
1107 (:map corfu-map
1108 ("TAB" . corfu-next)
1109 ("C-c" . corfu-quit)
1110 ([tab] . corfu-next)
1111 ("S-TAB" . corfu-previous)
1112 ([backtab] . corfu-previous))
1113 :hook
1114 (after-init . global-corfu-mode))
1115
1116(use-package corfu-history
1117 :after (corfu)
1118 :hook
1119 (after-init . corfu-history-mode))
1120
1121(use-package corfu-popupinfo
1122 :after corfu
1123 :init
1124 (declare-function corfu-popupinfo-mode "corfu-popupinfo")
1125 :config
1126 (corfu-popupinfo-mode 1))
1127
1128(use-package corfu-terminal
1129 :unless (display-graphic-p)
1130 :ensure t
1131 :hook
1132 (after-init . corfu-terminal-mode))
1133
1134(use-package envrc
1135 :defer 2
1136 :if (executable-find "direnv")
1137 :init
1138 (defvar envrc-mode-map)
1139 (declare-function envrc-global-mode "envrc")
1140 :bind (:map envrc-mode-map
1141 ("C-c e" . envrc-command-map))
1142 :custom
1143 (envrc-remote t)
1144 :config (envrc-global-mode))
1145
1146(use-package cape
1147 :init
1148 (declare-function cape-dabbrev "cape")
1149 (declare-function cape-file "cape")
1150 (declare-function cape-elisp-block "cape")
1151 (add-hook 'completion-at-point-functions #'cape-dabbrev)
1152 (add-hook 'completion-at-point-functions #'cape-file)
1153 (add-hook 'completion-at-point-functions #'cape-elisp-block))
1154
1155(use-package winner
1156 :unless noninteractive
1157 :hook
1158 (after-init . winner-mode))
1159
1160(use-package windmove
1161 :bind
1162 ("S-<up>" . windmove-up)
1163 ("S-<left>" . windmove-left)
1164 ("S-<right>" . windmove-right)
1165 ("S-<down>" . windmove-down)
1166 ("M-S-<up>" . windmove-swap-states-up)
1167 ("M-S-<left>" . windmove-swap-states-left)
1168 ("M-S-<right>" . windmove-swap-states-right)
1169 ("M-S-<down>" . windmove-swap-states-down))
1170
1171(use-package window
1172 :unless noninteractive
1173 :commands (shrink-window-horizontally shrink-window enlarge-window-horizontally enlarge-window)
1174 :bind (("S-C-<left>" . shrink-window-horizontally)
1175 ("S-C-<right>" . enlarge-window-horizontally)
1176 ("S-C-<down>" . shrink-window)
1177 ("S-C-<up>" . enlarge-window)))
1178
1179;; Prefer ripgrep (rg) if present (instead of grep)
1180(setq xref-search-program
1181 (cond
1182 ((or (executable-find "ripgrep")
1183 (executable-find "rg"))
1184 'ripgrep)
1185 ((executable-find "ugrep")
1186 'ugrep)
1187 (t
1188 'grep)))
1189
1190(use-package rg
1191 :if (executable-find "rg")
1192 :commands (rg rg-project rg-dwim)
1193 :bind (("M-s r r" . rg)
1194 ("M-s r p" . rg-project)
1195 ("M-s r s" . rg-dwim))
1196 :custom
1197 (rg-group-result t)
1198 (rg-hide-command t)
1199 (rg-show-columns nil)
1200 (rg-show-header t)
1201 (rg-default-alias-fallback "all")
1202 :config
1203 (defvar rg-custom-type-aliases)
1204 (defvar rg-buffer-name)
1205 (cl-pushnew '("tmpl" . "*.tmpl") rg-custom-type-aliases :test #'equal)
1206 (cl-pushnew '("gotest" . "*_test.go") rg-custom-type-aliases :test #'equal)
1207 (defun vde/rg-buffer-name ()
1208 "Generate a rg buffer name from project if in one"
1209 (let ((p (project-root (project-current))))
1210 (if p
1211 (format "rg: %s" (abbreviate-file-name p))
1212 "rg")))
1213 (declare-function vde/rg-buffer-name "init")
1214 (setq rg-buffer-name #'vde/rg-buffer-name))
1215
1216(use-package wgrep
1217 :unless noninteractive
1218 :commands (wgrep-change-to-wgrep-mode)
1219 :init
1220 (defvar grep-mode-map)
1221 :custom
1222 (wgrep-auto-save-buffer t)
1223 (wgrep-change-readonly-file t)
1224 :bind (:map grep-mode-map
1225 ("e" . wgrep-change-to-wgrep-mode)
1226 ("C-x C-q" . wgrep-change-to-wgrep-mode)))
1227
1228(use-package abbrev
1229 :ensure nil
1230 :custom
1231 (save-abbrevs nil)
1232 :config
1233 (define-abbrev-table 'global-abbrev-table
1234 '(;; Arrows
1235 ("ra" "→")
1236 ("la" "←")
1237 ("ua" "↑")
1238 ("da" "↓")
1239
1240 ;; Emojis for context markers
1241 ;; ("todo" "👷 TODO:")
1242 ;; ("fixme" "🔥 FIXME:")
1243 ;; ("note" "📎 NOTE:")
1244 ;; ("hack" "👾 HACK:")
1245 ("smile" "😄")
1246 ("party" "🎉")
1247 ("up" "☝️")
1248 ("applause" "👏")
1249 ("manyapplauses" "👏👏👏👏👏👏👏👏")
1250 ("heart" "❤️")
1251
1252 ;; NerdFonts
1253 ("nerdfolder" " ")
1254 ("nerdgit" "")
1255 ("nerdemacs" "")
1256
1257 ;; Markdown
1258 ("cb" "```@\n\n```"
1259 (lambda () (search-backward "@") (delete-char 1)))
1260
1261 ;; ORG
1262 ("ocb" "#+BEGIN_SRC @\n\n#+END_SRC"
1263 (lambda () (search-backward "@") (delete-char 1))))))
1264
1265(use-package tempel
1266 :custom (tempel-path (expand-file-name "templates" user-emacs-directory))
1267 :bind (("M-+" . tempel-complete) ;; Alternative tempel-expand
1268 ("M-*" . tempel-insert)))
1269
1270(use-package consult
1271 :bind
1272 ("M-g M-g" . consult-goto-line)
1273 ("M-K" . consult-keep-lines)
1274 ("M-s M-b" . consult-buffer)
1275 ("M-s M-f" . consult-find)
1276 ("M-s M-g" . consult-grep)
1277 ("M-s M-r" . consult-ripgrep)
1278 ("M-s M-h" . consult-history)
1279 ("M-s M-l" . consult-line)
1280 ("M-s M-m" . consult-mark)
1281 ("M-s M-y" . consult-yank-pop)
1282 ("M-s M-s" . consult-outline))
1283
1284(use-package consult-vc-modified-files
1285 :bind
1286 ("C-c v ." . consult-vc-log-select-files)
1287 ("C-c v m" . consult-vc-modified-files))
1288
1289(use-package avy
1290 :unless noninteractive
1291 :commands (avy-goto-char avy-goto-line avy-goto-word-1 avy-pop-mark avy-goto-char-timer)
1292 :bind (("C-c j w" . avy-goto-word-1)
1293 ("C-c j b" . avy-pop-mark)
1294 ("C-c j t" . avy-goto-char-timer)
1295 ("C-c j l" . avy-goto-line)))
1296
1297(use-package embark
1298 :unless noninteractive
1299 :commands (embark-act embark-dwim embark-prefix-help-command)
1300 :bind
1301 ("C-," . embark-act)
1302 ("M-," . embark-dwim)
1303 ("C-h b" . embark-bindings)
1304 ("C-h B" . embark-bindings-at-point)
1305 ("C-h M" . embark-bindings-in-keymap)
1306 (:map completion-list-mode-map
1307 ("," . embark-act))
1308 :custom
1309 (prefix-help-command #'embark-prefix-help-command)
1310 (embark-cycle-key ".")
1311 (embark-help-key "?")
1312 :config
1313 (defvar embark-action-indicator)
1314 (defvar embark-become-indicator)
1315 (defvar embark-symbol-map)
1316 (defvar embark-file-map)
1317 (defvar embark-buffer-map)
1318 (defvar embark-general-map)
1319 (defvar embark-keymap-alist)
1320 (defvar embark-target-finders)
1321 ;; which-key integration for better discoverability
1322 (declare-function which-key--show-keymap "which-key")
1323 (declare-function which-key--hide-popup-ignore-command "which-key")
1324 (setq embark-action-indicator
1325 (lambda (map _target)
1326 (which-key--show-keymap "Embark" map nil nil 'no-paging)
1327 #'which-key--hide-popup-ignore-command)
1328 embark-become-indicator embark-action-indicator)
1329
1330 ;; Symbol overlay actions
1331 (keymap-set embark-symbol-map "S i" #'symbol-overlay-put)
1332 (keymap-set embark-symbol-map "S c" #'symbol-overlay-remove-all)
1333 (keymap-set embark-symbol-map "S r" #'symbol-overlay-rename)
1334
1335 ;; File management actions
1336 (define-key embark-file-map "c" #'copy-file)
1337 (define-key embark-file-map "m" #'rename-file)
1338 (define-key embark-file-map "D" #'delete-file)
1339 (define-key embark-file-map "=" #'ediff-files)
1340
1341 ;; Open file as root
1342 (defun my/sudo-find-file (file)
1343 "Open FILE as root."
1344 (interactive "fFile: ")
1345 (find-file (concat "/sudo::" (expand-file-name file))))
1346 (declare-function my/sudo-find-file "init")
1347 (define-key embark-file-map "S" #'my/sudo-find-file)
1348
1349 ;; Buffer actions
1350 (defun my/kill-buffer-and-window ()
1351 "Kill current buffer and close its window."
1352 (interactive)
1353 (kill-buffer-and-window))
1354 (declare-function my/kill-buffer-and-window "init")
1355 (define-key embark-buffer-map "K" #'my/kill-buffer-and-window)
1356 (define-key embark-buffer-map "r" #'revert-buffer)
1357
1358 ;; Project/Git actions
1359 (define-key embark-file-map "p" #'project-find-file)
1360 (define-key embark-file-map "g" #'magit-file-dispatch)
1361
1362 (declare-function vc-root-dir "vc")
1363 (declare-function magit-status "magit")
1364 (defun my/magit-status-from-file ()
1365 "Open magit-status for the file's repository."
1366 (interactive)
1367 (magit-status (vc-root-dir)))
1368 (declare-function my/magit-status-from-file "init")
1369 (define-key embark-file-map "G" #'my/magit-status-from-file)
1370
1371 ;; Org-mode heading actions
1372 (declare-function org-refile "org")
1373 (declare-function org-todo "org")
1374 (declare-function org-schedule "org")
1375 (declare-function org-deadline "org")
1376 (declare-function org-archive-subtree "org")
1377 (declare-function org-narrow-to-subtree "org")
1378 (defvar-keymap embark-org-heading-map
1379 :doc "Actions for org headings"
1380 :parent embark-general-map
1381 "r" #'org-refile
1382 "t" #'org-todo
1383 "s" #'org-schedule
1384 "d" #'org-deadline
1385 "a" #'org-archive-subtree
1386 "n" #'org-narrow-to-subtree)
1387
1388 (add-to-list 'embark-keymap-alist '(org-heading . embark-org-heading-map))
1389
1390 ;; Custom target finders
1391 (declare-function thing-at-point-looking-at "thingatpt")
1392 (defun embark-target-github-issue ()
1393 "Target GitHub issue numbers like #123 or owner/repo#456."
1394 (when (thing-at-point-looking-at
1395 "\\(?:\\([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+\\)?#\\([0-9]+\\)\\)")
1396 (let ((_repo (match-string 1))
1397 (_number (match-string 2)))
1398 `(github-issue
1399 ,(match-string 0)
1400 ,(match-beginning 0)
1401 . ,(match-end 0)))))
1402
1403 (defun embark-target-jira-ticket ()
1404 "Target Jira tickets like PROJ-123."
1405 (when (thing-at-point-looking-at "\\([A-Z]+-[0-9]+\\)")
1406 `(jira-ticket
1407 ,(match-string 1)
1408 ,(match-beginning 0)
1409 . ,(match-end 0))))
1410
1411 (add-to-list 'embark-target-finders 'embark-target-github-issue)
1412 (add-to-list 'embark-target-finders 'embark-target-jira-ticket)
1413
1414 ;; GitHub issue actions
1415 (declare-function my/open-github-issue "init")
1416 (declare-function my/copy-github-issue-url "init")
1417 (defvar-keymap embark-github-issue-map
1418 :doc "Actions for GitHub issues/PRs"
1419 :parent embark-general-map
1420 "o" #'my/open-github-issue
1421 "c" #'my/copy-github-issue-url
1422 "b" #'browse-url)
1423
1424 (declare-function my/open-jira-ticket "init")
1425 (declare-function my/copy-jira-ticket-url "init")
1426 (defvar-keymap embark-jira-ticket-map
1427 :doc "Actions for Jira tickets"
1428 :parent embark-general-map
1429 "o" #'my/open-jira-ticket
1430 "c" #'my/copy-jira-ticket-url
1431 "b" #'browse-url)
1432
1433 (add-to-list 'embark-keymap-alist '(github-issue . embark-github-issue-map))
1434 (add-to-list 'embark-keymap-alist '(jira-ticket . embark-jira-ticket-map))
1435
1436 ;; Helper functions
1437 (defun my/open-github-issue (issue)
1438 "Open GitHub ISSUE in browser."
1439 (let* ((parts (split-string issue "#"))
1440 (repo (or (car parts) (vde/gh-get-current-repo)))
1441 (number (cadr parts))
1442 (url (format "https://github.com/%s/issues/%s" repo number)))
1443 (browse-url url)))
1444
1445 (defun my/copy-github-issue-url (issue)
1446 "Copy GitHub ISSUE URL to clipboard."
1447 (let* ((parts (split-string issue "#"))
1448 (repo (or (car parts) (vde/gh-get-current-repo)))
1449 (number (cadr parts))
1450 (url (format "https://github.com/%s/issues/%s" repo number)))
1451 (kill-new url)
1452 (message "Copied: %s" url)))
1453
1454 (defun my/open-jira-ticket (ticket)
1455 "Open Jira TICKET in browser."
1456 (let ((url (format "https://issues.redhat.com/browse/%s" ticket)))
1457 (browse-url url)))
1458
1459 (defun my/copy-jira-ticket-url (ticket)
1460 "Copy Jira TICKET URL to clipboard."
1461 (let ((url (format "https://issues.redhat.com/browse/%s" ticket)))
1462 (kill-new url)
1463 (message "Copied: %s" url)))
1464 )
1465
1466(use-package embark-consult
1467 :after (embark consult)
1468 :unless noninteractive
1469 :hook
1470 (embark-collect-mode . consult-preview-at-point-mode))
1471
1472(use-package consult-gh
1473 :after (consult)
1474 :custom
1475 (consult-gh-show-preview t)
1476 (consult-gh-preview-key "C-o")
1477 (consult-gh-large-file-warning-threshold 2500000)
1478 (consult-gh-default-interactive-command #'consult-gh-transient)
1479 (consult-gh-prioritize-local-folder nil)
1480 (consult-gh-group-dashboard-by :reason)
1481 ;;;; Optional
1482 (consult-gh-repo-preview-major-mode nil) ; show readmes in their original format
1483 (consult-gh-preview-major-mode 'org-mode) ; use 'org-mode for editing comments, commit messages, ...
1484 :config
1485 ;; Remember visited orgs and repos across sessions
1486 (add-to-list 'savehist-additional-variables 'consult-gh--known-orgs-list)
1487 (add-to-list 'savehist-additional-variables 'consult-gh--known-repos-list))
1488
1489;; (consult-gh-issue-list "tektoncd/pipeline -- --assignee \"@me\"" t)
1490;; (consult-gh-pr-list "tektoncd/pipeline -- --assignee \"@me\"" t)
1491
1492(use-package consult-gh-embark
1493 :after (embark consult)
1494 :init
1495 (declare-function consult-gh-embark-mode "consult-gh-embark")
1496 :config
1497 (consult-gh-embark-mode +1))
1498
1499(use-package pr-review
1500 :commands (pr-review pr-review-open pr-review-submit-review)
1501 :custom
1502 (pr-review-ghub-host "api.github.com")
1503 (pr-review-notification-include-read nil)
1504 (pr-review-notification-include-unsubscribed nil))
1505
1506(use-package pr-review-search
1507 :commands (pr-review-search pr-review-search-open pr-review-current-repository pr-review-current-repository-search)
1508 :config
1509 (defun pr-review-current-repository-search (query)
1510 "Run pr-review-search on the current repository."
1511 (interactive "sSearch query: ")
1512 (pr-review-search (format "is:pr archived:false is:open repo:%s %s" (vde/gh-get-current-repo) query)))
1513
1514 (defun pr-review-current-repository ()
1515 "Run pr-review-search on the current repository."
1516 (interactive)
1517 (pr-review-search (format "is:pr archived:false is:open repo:%s" (vde/gh-get-current-repo)))))
1518
1519(use-package jinx
1520 :hook (emacs-startup . global-jinx-mode)
1521 :bind (([remap ispell-word] . jinx-correct) ;; ("M-$" . jinx-correct)
1522 ("C-M-$" . jinx-languages)))
1523
1524(use-package eljira
1525 :commands (eljira)
1526 :ensure nil
1527 :load-path "~/src/github.com/sawwheet/eljira/"
1528 :custom
1529 (eljira-token (passage-get "redhat/issues/token/myji"))
1530 (eljira-username "vdemeest@redhat.com")
1531 (eljira-url "https://issues.redhat.com"))
1532
1533(use-package chatgpt-shell
1534 :commands (chatgpt-shell)
1535 :custom
1536 (chatgpt-shell-google-key (passage-get "ai/gemini/api_key"))
1537 (chatgpt-shell-openrouter-key (passage-get "ai/openroute/api_key"))
1538 (chatgpt-shell-deepseek-key (passage-get "ai/deepseek/api_key")))
1539
1540;; TODO window management
1541;; TODO ORG mode configuration (BIG one)
1542(defvar org-link-any-re)
1543(declare-function dom-by-tag "dom")
1544(declare-function dom-text "dom")
1545(declare-function org-in-regexp "org")
1546(declare-function org-make-link-string "org")
1547(defun ar/org-insert-link-dwim ()
1548 "Like `org-insert-link' but with personal dwim preferences."
1549 (interactive)
1550 (let* ((point-in-link (org-in-regexp org-link-any-re 1))
1551 (clipboard-url (when (string-match-p "^http" (current-kill 0))
1552 (current-kill 0)))
1553 (region-content (when (region-active-p)
1554 (buffer-substring-no-properties (region-beginning)
1555 (region-end)))))
1556 (cond ((and region-content clipboard-url (not point-in-link))
1557 (delete-region (region-beginning) (region-end))
1558 (insert (org-make-link-string clipboard-url region-content)))
1559 ((and clipboard-url (not point-in-link))
1560 (insert (org-make-link-string
1561 clipboard-url
1562 (read-string "title: "
1563 (with-current-buffer (url-retrieve-synchronously clipboard-url)
1564 (dom-text (car
1565 (dom-by-tag (libxml-parse-html-region
1566 (point-min)
1567 (point-max))
1568 'title))))))))
1569 (t
1570 (call-interactively 'org-insert-link)))))
1571
1572(use-package org
1573 :if (file-exists-p org-directory)
1574 :init
1575 (declare-function bind-key--remove "bind-key")
1576 :mode (("\\.org$" . org-mode)
1577 ("\\.org.draft$" . org-mode))
1578 :commands (org-agenda org-capture)
1579 :bind (("C-c o l" . org-store-link)
1580 ("C-c o r r" . org-refile)
1581 ;; ("C-c o r R" . vde/reload-org-refile-targets)
1582 ("C-c o a a" . org-agenda)
1583 ;; ("C-c o a r" . vde/reload-org-agenda-files)
1584 ;; ("C-c C-x i" . vde/org-clock-in-any-heading)
1585 ("C-c o s" . org-sort)
1586 ("C-c O" . org-open-at-point-global)
1587 ("<f12>" . org-agenda)
1588 :map org-mode-map
1589 ("C-c C-l" . ar/org-insert-link-dwim))
1590 :custom
1591 (org-use-speed-commands t)
1592 (org-special-ctrl-a/e t)
1593 (org-special-ctrl-k t)
1594 (org-hide-emphasis-markers t)
1595 (org-pretty-entities t)
1596 (org-ellipsis "…")
1597 (org-return-follows-link t)
1598 (org-todo-keywords '((sequence "STRT(s)" "NEXT(n)" "TODO(t)" "WAIT(w)" "|" "DONE(d!)" "CANX(c@/!)")))
1599 (org-todo-state-tags-triggers '(("CANX" ("CANX" . t))
1600 ("WAIT" ("WAIT" . t))
1601 (done ("WAIT"))
1602 ("TODO" ("WAIT") ("CANX"))
1603 ("NEXT" ("WAIT") ("CANX"))
1604 ("DONE" ("WAIT") ("CANX"))))
1605 (org-tag-alist
1606 '((:startgroup)
1607 ("Handson" . ?o)
1608 (:grouptags)
1609 ("Write" . ?w) ("Code" . ?c)
1610 (:endgroup)
1611
1612 (:startgroup)
1613 ("Handsoff" . ?f)
1614 (:grouptags)
1615 ("Read" . ?r) ("Watch" . ?W) ("Listen" . ?l)
1616 (:endgroup)
1617
1618 ;; Eisenhower Matrix tags
1619 ("urgent" . ?u)
1620 ("important" . ?i)))
1621 (org-log-done 'time)
1622 (org-log-redeadline 'time)
1623 (org-log-reschedule 'time)
1624 (org-log-into-drawer t)
1625 ;; https://jeffbradberry.com/posts/2025/05/orgmode-priority-cookies/
1626 ;; 1 2 and 3 are high, 4 is default, 5 is "hide / whenever or maybe never"
1627 (org-priority-highest 1)
1628 (org-priority-lowest 5)
1629 (org-priority-default 4)
1630 (org-list-demote-modify-bullet '(("+" . "-") ("-" . "+")))
1631 (org-agenda-file-regexp "^[a-zA-Z0-9-_]+.org$")
1632 (org-agenda-files `(,org-inbox-file ,org-todos-file ,org-habits-file ,org-reading-list-file ,org-calendar-file))
1633 ;; (org-refile-targets '((org-agenda-files :maxlevel . 3)))
1634 (org-refile-targets (vde/org-refile-targets))
1635 (org-refile-use-outline-path 'file)
1636 (org-refile-allow-creating-parent-nodes 'confirm)
1637 (org-agenda-remove-tags t)
1638 (org-agenda-span 'day)
1639 (org-agenda-start-on-weekday 1)
1640 (org-agenda-window-setup 'current-window)
1641 (org-agenda-sticky t)
1642 (org-agenda-sorting-strategy
1643 '((agenda time-up deadline-up scheduled-up todo-state-up priority-down)
1644 (todo todo-state-up priority-down deadline-up)
1645 (tags todo-state-up priority-down deadline-up)
1646 (search todo-state-up priority-down deadline-up)))
1647 (org-agenda-custom-commands
1648 '(
1649 ;; Archive tasks
1650 ("#" "To archive" todo "DONE|CANX")
1651 ;; TODO take inspiration from those
1652 ;; ("$" "Appointments" agenda* "Appointments")
1653 ;; ("b" "Week tasks" agenda "Scheduled tasks for this week"
1654 ;; ((org-agenda-category-filter-preset '("-RDV")) ; RDV for Rendez-vous
1655 ;; (org-agenda-use-time-grid nil)))
1656 ;;
1657 ;; ;; Review started and next tasks
1658 ;; ("j" "STRT/NEXT" tags-todo "TODO={STRT\\|NEXT}")
1659 ;;
1660 ;; ;; Review other non-scheduled/deadlined to-do tasks
1661 ;; ("k" "TODO" tags-todo "TODO={TODO}+DEADLINE=\"\"+SCHEDULED=\"\"")
1662 ;;
1663 ;; ;; Review other non-scheduled/deadlined pending tasks
1664 ;; ("l" "WAIT" tags-todo "TODO={WAIT}+DEADLINE=\"\"+SCHEDULED=\"\"")
1665 ;;
1666 ;; ;; Review upcoming deadlines for the next 60 days
1667 ;; ("!" "Deadlines all" agenda "Past/upcoming deadlines"
1668 ;; ((org-agenda-span 1)
1669 ;; (org-deadline-warning-days 60)
1670 ;; (org-agenda-entry-types '(:deadline))))
1671
1672 ("d" "Daily Agenda"
1673 ((agenda ""
1674 ((org-agenda-span 'day)
1675 (org-deadline-warning-days 5)))
1676 (tags-todo "+PRIORITY=\"1\""
1677 ((org-agenda-overriding-header "High Priority Tasks")))
1678 (todo "NEXT"
1679 ((org-agenda-overriding-header "Next Tasks")))))
1680 ("D" "Daily Agenda (old)"
1681 ((agenda ""
1682 ((org-agenda-files (vde/all-org-agenda-files))
1683 (org-agenda-span 'day)
1684 (org-deadline-warning-days 5)))
1685 (tags-todo "+PRIORITY=\"A\""
1686 ((org-agenda-files (vde/all-org-agenda-files))
1687 (org-agenda-overriding-header "High Priority Tasks")))
1688 (todo "NEXT"
1689 ((org-agenda-files (vde/all-org-agenda-files))
1690 (org-agenda-overriding-header "Next Tasks")))))
1691 ("i" "Inbox (triage)"
1692 ((tags-todo ".*"
1693 ((org-agenda-files `(,org-inbox-file)) ;; FIXME use constant here
1694 (org-agenda-overriding-header "Unprocessed Inbox Item")))))
1695 ("A" "All (old)"
1696 ((tags-todo ".*"
1697 ((org-agenda-files (vde/all-org-agenda-files))))))
1698 ("u" "Untagged Tasks"
1699 ((tags-todo "-{.*}"
1700 ((org-agenda-overriding-header "Untagged tasks")))))
1701 ("w" "Weekly Review"
1702 ((agenda ""
1703 ((org-agenda-overriding-header "Completed Tasks")
1704 (org-agenda-skip-function '(org-agenda-skip-entry-if 'nottodo 'done))
1705 (org-agenda-span 'week)))
1706 (agenda ""
1707 ((org-agenda-overriding-header "Unfinished Scheduled Tasks")
1708 (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
1709 (org-agenda-span 'week)))))
1710 ;; FIXME Should only take into account projects and areas ?
1711 ("R" "Review projects" tags-todo "-CANX/"
1712 ((org-agenda-overriding-header "Reviews Scheduled")
1713 (org-agenda-skip-function 'org-review-agenda-skip)
1714 (org-agenda-cmp-user-defined 'org-review-compare)
1715 (org-agenda-sorting-strategy '(user-defined-down))))
1716
1717 ;; Eisenhower Matrix views
1718 ("e" "Eisenhower Matrix"
1719 ((tags-todo "+urgent+important"
1720 ((org-agenda-overriding-header "Q1: Do First (Urgent & Important)")))
1721 (tags-todo "+important-urgent"
1722 ((org-agenda-overriding-header "Q2: Schedule (Important, Not Urgent)")))
1723 (tags-todo "+urgent-important"
1724 ((org-agenda-overriding-header "Q3: Delegate (Urgent, Not Important)")))
1725 (tags-todo "-urgent-important"
1726 ((org-agenda-overriding-header "Q4: Eliminate (Neither Urgent nor Important)")))))
1727
1728 ;; Individual Eisenhower quadrants
1729 ("1" "Q1: Do First" tags-todo "+urgent+important")
1730 ("2" "Q2: Schedule" tags-todo "+important-urgent")
1731 ("3" "Q3: Delegate" tags-todo "+urgent-important")
1732 ("4" "Q4: Eliminate" tags-todo "-urgent-important")))
1733 ;; TODO cleanup this list a bit
1734 (org-agenda-category-icon-alist `(("personal" ,(list (propertize "🏡")))
1735 ("work" ,(list (propertize "🏢")))
1736 ("appointments" ,(list (propertize "📅")))
1737 ("health" ,(list (propertize "⚕️")))
1738 ("systems" ,(list (propertize "🖥️")))
1739 ("journal" ,(list (propertize "📝")))
1740 ("project--" ,(list (propertize "💼" )))
1741 ("tekton", (list (propertize "😼")))
1742 ("openshift-pipelines", (list (propertize "🎩")))
1743 ("redhat", (list (propertize "🎩")))
1744 ("area--" ,(list (propertize"🏢" )))
1745 ("area--home" ,(list (propertize "🏡")))
1746 ("home" ,(list (propertize "🏡")))
1747 ("home-services" ,(list (propertize "☕ ")))
1748 ("email" ,(list (propertize"📨" )))
1749 ("people" ,(list (propertize"👤" )))
1750 ("machine" ,(list (propertize "🖥️")))
1751 ("website" ,(list (propertize "🌍")))
1752 ("bike" ,(list (propertize "🚴♂️")))
1753 ("security" ,(list (propertize "🛡️")))
1754 ("i*" ,(list (propertize "📒")))))
1755 (org-agenda-prefix-format '((agenda . " %i %?-12t% s") ;; " %i %-12:c%?-12t%s%(my/org-agenda-repeater)"
1756 (todo . " %i")
1757 (tags . " %i")
1758 (search . " %i")))
1759 ;; (org-agenda-scheduled-leaders '("Sched." "S.%2dx"))
1760 ;; (org-agenda-deadline-leaders '("Deadl." "In%2dd" "D.%2dx"))
1761 (org-insert-heading-respect-content t)
1762 (org-M-RET-may-split-line '((default . nil)))
1763 (org-goto-interface 'outline-path-completion)
1764 (org-outline-path-complete-in-steps nil)
1765 (org-goto-max-level 2)
1766 :hook
1767 (org-mode . auto-fill-mode)
1768 (org-mode . auto-revert-mode)
1769 (org-mode . visual-line-mode)
1770 :bind
1771 (:map org-mode-map
1772 ("C-<left>" . org-shiftleft)
1773 ("C-<right>" . org-shiftright)
1774 ("C-<up>" . org-shiftup)
1775 ("C-<down>" . org-shiftdown)
1776 ("C-c e" . vde/org-eisenhower-set-tags)
1777 ("C-c C-S-o" . vde/org-open-at-point-in-new-tab))
1778 :config
1779 ;; Unbind C-, to allow embark-act to work
1780 (unbind-key "C-," org-mode-map)
1781
1782 (unbind-key "S-<left>" org-mode-map)
1783 (unbind-key "S-<right>" org-mode-map)
1784 (unbind-key "S-<up>" org-mode-map)
1785 (unbind-key "S-<down>" org-mode-map)
1786 (unbind-key "M-S-<left>" org-mode-map)
1787 (unbind-key "M-S-<right>" org-mode-map)
1788 (unbind-key "M-S-<up>" org-mode-map)
1789 (unbind-key "M-S-<down>" org-mode-map)
1790 (unbind-key "C-S-<left>" org-mode-map)
1791 (unbind-key "C-S-<right>" org-mode-map)
1792 (unbind-key "C-S-<up>" org-mode-map)
1793 (unbind-key "C-S-<down>" org-mode-map)
1794
1795 (defun vde/org-open-at-point-in-new-tab ()
1796 "Open org link at point in a new tab-bar tab.
1797Works with any org link type. Creates a new tab, then opens the link."
1798 (interactive)
1799 (let ((link-info (org-element-context)))
1800 (when (eq (org-element-type link-info) 'link)
1801 (tab-bar-new-tab)
1802 (org-open-at-point))))
1803
1804 (declare-function org-before-first-heading-p "org")
1805 (declare-function org-get-repeat "org")
1806 (defun my/org-agenda-repeater ()
1807 "Return the repeater string for the current agenda entry."
1808 (if (org-before-first-heading-p)
1809 "-------" ; Align with the time grid
1810 (format "%5s: " (or (org-get-repeat) ""))))
1811
1812 (require 'dash)
1813 (require 's)
1814 (declare-function --remove "dash")
1815 (declare-function --map "dash")
1816 (declare-function ->> "dash")
1817 (declare-function s-starts-with? "s")
1818 (declare-function s-contains? "s")
1819 (defun vde/org-refile-targets ()
1820 (append '((org-inbox-file :level . 0)
1821 (org-todos-file :maxlevel . 3)
1822 (org-habits-file :maxlevel . 3))
1823 (->>
1824 (directory-files org-notes-directory nil ".org$")
1825 (--remove (s-starts-with? "." it))
1826 (--remove (s-contains? "==readwise=" it))
1827 (--map (format "%s/%s" org-notes-directory it))
1828 (--map `(,it :maxlevel . 3)))
1829 ))
1830
1831 (defun vde/org-eisenhower-set-tags ()
1832 "Quickly set Eisenhower Matrix tags (urgent/important) on current heading."
1833 (interactive)
1834 (let* ((choices '((?1 . ("+urgent" "+important")) ; Q1: Do First
1835 (?2 . ("+important" "-urgent")) ; Q2: Schedule
1836 (?3 . ("+urgent" "-important")) ; Q3: Delegate
1837 (?4 . ("-urgent" "-important")) ; Q4: Eliminate
1838 (?u . ("+urgent")) ; Just urgent
1839 (?i . ("+important")) ; Just important
1840 (?c . ("-urgent" "-important")))) ; Clear both
1841 (key (read-char-choice
1842 "Quadrant: [1]Do [2]Schedule [3]Delegate [4]Eliminate | [u]rgent [i]mportant [c]lear: "
1843 '(?1 ?2 ?3 ?4 ?u ?i ?c)))
1844 (tags (cdr (assoc key choices))))
1845 (dolist (tag tags)
1846 (org-toggle-tag (substring tag 1) (if (string-prefix-p "+" tag) 'on 'off))))))
1847
1848(use-package org-appear
1849 :hook (org-mode . org-appear-mode))
1850
1851(use-package org-agenda
1852 :after org
1853 :commands (org-agenda)
1854 :config
1855 (unbind-key "S-<left>" org-agenda-mode-map)
1856 (unbind-key "S-<right>" org-agenda-mode-map)
1857 (unbind-key "S-<up>" org-agenda-mode-map)
1858 (unbind-key "S-<down>" org-agenda-mode-map)
1859 (unbind-key "C-S-<left>" org-agenda-mode-map)
1860 (unbind-key "C-S-<right>" org-agenda-mode-map))
1861
1862;; Make sure we load org-protocol
1863(use-package org-protocol
1864 :after org)
1865
1866(use-package org-tempo
1867 :after (org)
1868 :custom
1869 (org-structure-template-alist '(("a" . "aside")
1870 ("c" . "center")
1871 ("C" . "comment")
1872 ("e" . "example")
1873 ("E" . "export")
1874 ("Ea" . "export ascii")
1875 ("Eh" . "export html")
1876 ("El" . "export latex")
1877 ("q" . "quote")
1878 ("s" . "src")
1879 ("se" . "src emacs-lisp")
1880 ("sE" . "src emacs-lisp :results value code :lexical t")
1881 ("sg" . "src go")
1882 ("sr" . "src rust")
1883 ("sp" . "src python")
1884 ("v" . "verse"))))
1885
1886(use-package org-capture
1887 :commands (org-capture)
1888 :bind (("C-c o c" . org-capture))
1889 :config
1890 (add-to-list 'org-capture-templates
1891 `("t" "📥 Tasks")
1892 t)
1893 (add-to-list 'org-capture-templates
1894 `("tt" " New task" entry
1895 (file ,org-inbox-file)
1896 "* TODO %?\n:PROPERTIES:\n:CREATED:\t%U\n:END:\n\n%i\n\nFrom: %a"
1897 :empty-lines 1)
1898 t)
1899 (add-to-list 'org-capture-templates
1900 `("tl" " New task (from capture)" entry
1901 (file ,org-inbox-file)
1902 "* TODO %a\n:PROPERTIES:\n:CREATED:\t%U\n:END:\n\n%i\n\nFrom: %a"
1903 :empty-lines 1)
1904 t)
1905 (add-to-list 'org-capture-templates
1906 `("td" "✅ Done" entry
1907 (file ,org-inbox-file)
1908 "* DONE %?\n:PROPERTIES:\n:CREATED:\t%U\n:END:\n\n%i\n\nFrom: %a"
1909 :empty-lines 1)
1910 t)
1911 ;; Refine this
1912 (add-to-list 'org-capture-templates
1913 `("tr" " PR Review" entry
1914 (file ,org-inbox-file)
1915 "* TODO review gh:%^{issue} :review:\n:PROPERTIES:\n:CREATED:%U\n:END:\n\n%i\n%?\nFrom: %a"
1916 :empty-lines 1)
1917 t)
1918 (add-to-list 'org-capture-templates
1919 `("l" "🔗 Link" entry
1920 (file ,org-inbox-file)
1921 "* %a\n%U\n%?\n%i"
1922 :empty-lines 1)
1923 t)
1924 (add-to-list 'org-capture-templates
1925 `("m" "✉ Email Workflow")
1926 t)
1927 ;; Forward declaration - defined in mu4e config
1928 (declare-function vde-mu4e--body-summary-for-capture "init")
1929 (add-to-list 'org-capture-templates
1930 `("mf" "Follow Up" entry
1931 (file ,org-inbox-file)
1932 "* TODO Follow up: %:subject :email:
1933:PROPERTIES:
1934:CREATED:\t%U
1935:FROM:\t%:from
1936:DATE:\t%:date
1937:END:
1938SCHEDULED:%t
1939DEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))
1940%(vde-mu4e--body-summary-for-capture)
1941%i
1942Link: %a"
1943 :immediate-finish t)
1944 t)
1945 (add-to-list 'org-capture-templates
1946 `("mr" "Read Later" entry
1947 (file ,org-inbox-file)
1948 "* TODO Read: %:subject :email:
1949:PROPERTIES:
1950:CREATED:\t%U
1951:FROM:\t%:from
1952:DATE:\t%:date
1953:END:
1954SCHEDULED:%t
1955DEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))
1956%(vde-mu4e--body-summary-for-capture)
1957%i
1958Link: %a"
1959 :immediate-finish t)
1960 t)
1961 (add-to-list 'org-capture-templates
1962 `("mt" "Task from email" entry
1963 (file ,org-inbox-file)
1964 "* TODO %:subject :email:
1965:PROPERTIES:
1966:CREATED:\t%U
1967:FROM:\t%:from
1968:DATE:\t%:date
1969:END:
1970%(vde-mu4e--body-summary-for-capture)
1971%?
1972Link: %a"
1973 :empty-lines 1)
1974 t)
1975
1976 (defvar vde/org-capture-refile-templates '("mt")
1977 "List of capture template keys that should trigger refile after capture.")
1978
1979 (defun vde/org-capture-refile-if-needed ()
1980 "Refile the captured entry if the template key is in `vde/org-capture-refile-templates'."
1981 (when (and (not org-note-abort)
1982 (member (plist-get org-capture-plist :key) vde/org-capture-refile-templates))
1983 (org-capture-goto-last-stored)
1984 (call-interactively 'org-refile)))
1985
1986 (add-hook 'org-capture-after-finalize-hook #'vde/org-capture-refile-if-needed)
1987 (add-to-list 'org-capture-templates
1988 `("J" "🗞 Journal (antidated) entry" item
1989 (file+datetree+prompt ,org-journal-file)
1990 "%U %?\n%i")
1991 t)
1992 ;; TODO: refine this, create a function that reset this
1993 ;; emails
1994 ;; (add-to-list 'org-capture-templates
1995 ;; `("m" "Meeting notes" entry
1996 ;; (file+datetree ,org-meeting-notes-file)
1997 ;; (file ,(concat user-emacs-directory "/etc/orgmode/meeting-notes.org"))))
1998
1999 (defun vde/window-delete-popup-frame (&rest _)
2000 "Kill selected selected frame if it has parameter `prot-window-popup-frame'.
2001Use this function via a hook."
2002 (when (frame-parameter nil 'vde/window-popup-frame)
2003 (delete-frame)))
2004
2005 ;; (add-to-list 'org-capture-templates
2006 ;; `("w" "Writing"))
2007 (declare-function vde/window-delete-popup-frame "init")
2008 (add-hook 'org-capture-after-finalize-hook #'vde/window-delete-popup-frame))
2009
2010;; Journelly - Smart journal capture with location/weather
2011(use-package journelly
2012 :after org-capture
2013 :demand t)
2014
2015(use-package org-habit
2016 :after org
2017 :custom
2018 (org-habit-show-habits-only-for-today nil)
2019 (org-habit-graph-column 80))
2020
2021(use-package ox-tufte
2022 :after org
2023 :ensure t
2024 :commands (org-tufte-export-to-html
2025 org-tufte-export-as-html
2026 vde/org-add-tufte-headers
2027 vde/org-add-tufte-headers-to-file
2028 vde/org-export-notes-with-export-tag)
2029 :custom
2030 ;; Path to tufte.css relative to website root
2031 (org-tufte-htmlize-output-type 'css)
2032 :config
2033 ;; Default HTML head includes for Tufte CSS
2034 (setq org-html-head-include-default-style nil)
2035 (setq org-html-head-include-scripts nil)
2036
2037 (defun vde/org-add-tufte-headers ()
2038 "Add Tufte CSS export headers to current org-mode buffer.
2039Inserts the headers after the frontmatter (#+title, #+date, etc.)
2040if they don't already exist."
2041 (interactive)
2042 (unless (derived-mode-p 'org-mode)
2043 (user-error "Not in an org-mode buffer"))
2044 (save-excursion
2045 ;; Check if headers already exist
2046 (goto-char (point-min))
2047 (if (re-search-forward "^#\\+HTML_HEAD:.*tufte\\.css" nil t)
2048 (message "Tufte headers already present")
2049 ;; Find insertion point after frontmatter
2050 (goto-char (point-min))
2051 ;; Skip past all the #+KEYWORD: lines at the top
2052 (while (and (not (eobp))
2053 (looking-at "^#\\+\\(title\\|date\\|filetags\\|identifier\\|category\\|author\\|email\\|description\\):"))
2054 (forward-line 1))
2055 ;; Insert headers
2056 (insert "#+HTML_HEAD: <link rel=\"stylesheet\" href=\"tufte.css\" type=\"text/css\" />\n")
2057 (insert "#+HTML_HEAD: <link rel=\"stylesheet\" href=\"ox-tufte.css\" type=\"text/css\" />\n")
2058 (insert "#+OPTIONS: toc:nil num:nil html-style:nil html-scripts:nil\n")
2059 (message "Tufte headers added"))))
2060
2061 (defun vde/org-add-tufte-headers-to-file (file)
2062 "Add Tufte CSS export headers to an org-mode FILE."
2063 (interactive "fOrg file: ")
2064 (with-current-buffer (find-file-noselect file)
2065 (vde/org-add-tufte-headers)
2066 (save-buffer)))
2067
2068 (defun vde/org-export-notes-with-export-tag (notes-dir output-dir)
2069 "Export all org files in NOTES-DIR with _export tag to OUTPUT-DIR using ox-tufte.
2070Creates OUTPUT-DIR if it doesn't exist."
2071 (interactive "DNotes directory: \nDOutput directory: ")
2072 (unless (file-directory-p output-dir)
2073 (make-directory output-dir t))
2074 (let ((files (directory-files notes-dir t "\\.org$"))
2075 (exported 0))
2076 (dolist (file files)
2077 (with-temp-buffer
2078 (insert-file-contents file)
2079 (when (re-search-forward ":_export:" nil t)
2080 (let* ((basename (file-name-base file))
2081 (output-file (expand-file-name
2082 (concat basename ".html")
2083 output-dir)))
2084 (with-current-buffer (find-file-noselect file)
2085 (org-tufte-export-to-html)
2086 (when (file-exists-p (concat (file-name-sans-extension file) ".html"))
2087 (rename-file (concat (file-name-sans-extension file) ".html")
2088 output-file t)
2089 (message "Exported: %s -> %s" (file-name-nondirectory file) output-file)
2090 (setq exported (1+ exported))))))))
2091 (message "Exported %d file(s) with _export tag" exported))))
2092
2093(use-package ob-ditaa
2094 :after org
2095 :commands (org-babel-execute:ditaa)
2096 :config
2097 (setq org-ditaa-jar-path "/home/vincent/local/state/nix/profiles/home-manager/home-path/lib/ditaa.jar"))
2098
2099(use-package ob-dot
2100 :after org
2101 :commands (org-babel-execute:dot))
2102
2103(use-package xeft
2104 :commands (xeft)
2105 :custom
2106 (xeft-directory org-notes-directory)
2107 (xeft-recursive 'follow-symlinks)
2108 (xeft-extensions '("md" "org")))
2109
2110(use-package denote
2111 :commands (denote)
2112 :init
2113 (declare-function denote-rename-buffer-mode "denote")
2114 :bind (("C-c n c" . denote-region)
2115 ("C-c n i" . denote-link-or-create)
2116 ("C-c n b" . denote-backlinks)
2117 ("C-c n F f" . denote-find-link)
2118 ("C-c n F b" . denote-find-backlink)
2119 ("C-c n r" . vde/org-refile-to-denote))
2120 :custom
2121 (denote-directory org-notes-directory)
2122 (denote-rename-buffer-format "📝 %t")
2123 (denote-date-prompt-denote-date-prompt-use-org-read-date t)
2124 (denote-prompts '(title keywords))
2125 (denote-backlinks-display-buffer-action
2126 '((display-buffer-reuse-window
2127 display-buffer-in-side-window)
2128 (side . bottom)
2129 (slot . 99)
2130 (window-width . 0.3)
2131 (dedicated . t)
2132 (preserve-size . (t . t))))
2133 :hook (dired-mode . denote-dired-mode)
2134 :config
2135 (denote-rename-buffer-mode 1)
2136 (declare-function denote-file-is-note-p "denote")
2137 (declare-function denote-rename-file-using-front-matter "denote")
2138 (defun my-denote-always-rename-on-save-based-on-front-matter ()
2139 "Rename the current Denote file, if needed, upon saving the file.
2140Rename the file based on its front matter, checking for changes in the
2141title or keywords fields.
2142
2143Add this function to the `after-save-hook'."
2144 (let ((_denote-rename-confirmations nil)
2145 (_denote-save-buffers t)) ; to save again post-rename
2146 (when (and buffer-file-name (denote-file-is-note-p buffer-file-name))
2147 (ignore-errors (denote-rename-file-using-front-matter buffer-file-name))
2148 (message "Buffer saved; Denote file renamed"))))
2149
2150 (declare-function my-denote-always-rename-on-save-based-on-front-matter "init")
2151 (add-hook 'after-save-hook #'my-denote-always-rename-on-save-based-on-front-matter)
2152
2153 (declare-function denote-sluggify "denote")
2154 (declare-function denote--retrieve-title-or-filename "denote")
2155 (defun vde/org-category-from-buffer ()
2156 "Get the org category (#+category:) value from the buffer"
2157 (cond
2158 ((string-match "__journal.org$" (buffer-file-name))
2159 "journal")
2160 (t
2161 (denote-sluggify (denote--retrieve-title-or-filename (buffer-file-name) 'org)))))
2162
2163 (declare-function org-refile-get-targets "org")
2164 (declare-function org-refile-cache-clear "org")
2165 (declare-function denote-retrieve-title-value "denote")
2166 (declare-function denote-directory-files "denote")
2167 (declare-function denote-title-prompt "denote")
2168 (defun vde/org-refile-to-denote ()
2169 "Refile to a denote note (existing or new)."
2170 (interactive)
2171 (let* ((source-buffer (current-buffer))
2172 (files (denote-directory-files))
2173 (choices (mapcar (lambda (f)
2174 (cons (denote-retrieve-title-value f 'org) f))
2175 files))
2176 (selection (completing-read "Refile to note: " (mapcar #'car choices)))
2177 (target (if-let ((match (cdr (assoc selection choices))))
2178 match
2179 (cl-letf (((symbol-function 'denote-title-prompt)
2180 (lambda (&rest _) selection)))
2181 (call-interactively #'denote))))
2182 (target-buffer (find-file-noselect target)))
2183 (with-current-buffer target-buffer
2184 (org-mode)
2185 (save-excursion
2186 (goto-char (point-min))
2187 (unless (re-search-forward "^\\*+ " nil t)
2188 (goto-char (point-max))
2189 (insert "\n* Notes\n")))
2190 (save-buffer))
2191 (with-current-buffer source-buffer
2192 (let ((org-refile-targets `((,target :maxlevel . 3)))
2193 (org-refile-use-outline-path 'file)
2194 (org-refile-target-verify-function nil))
2195 (org-refile-cache-clear)
2196 ;; Force org to rebuild targets
2197 (org-refile-get-targets)
2198 (call-interactively #'org-refile))))))
2199
2200(use-package denote-org
2201 :after (denote org)
2202 :defer 2)
2203
2204(use-package popper
2205 :commands (popper-mode)
2206 :bind (("C-#" . popper-toggle)
2207 ;; ("C-~" . popper-kill-latest-popup)
2208 ("M-#" . popper-cycle)
2209 ;; alt keybind for mac mode, possibly disposable? TODO: should bind only on mac?
2210 ("C-`" . popper-toggle-latest)
2211 ("C-M-#" . popper-toggle-type))
2212 :custom
2213 (popper-reference-buffers
2214 '("\\*Messages\\*"
2215 "\\*Warnings\\*"
2216 "Output\\*$"
2217 "\\*Async Shell Command\\*"
2218 help-mode
2219 compilation-mode))
2220 (popper-window-height 0.3)
2221 :config
2222 (popper-mode))
2223
2224(use-package popper-echo
2225 :commands (popper-echo-mode popper-tab-line-mode)
2226 :init
2227 (popper-tab-line-mode))
2228
2229(use-package mu4e
2230 :commands (mu4e)
2231 :init
2232 (declare-function make-mu4e-context "mu4e")
2233 (declare-function mu4e~draft-message-filename-construct "mu4e")
2234 (declare-function mu4e~proc-add "mu4e")
2235 (declare-function mu4e~mark-check-target "mu4e")
2236 (declare-function mu4e-flags-to-string "mu4e")
2237 (declare-function mu4e-message-field "mu4e")
2238 (declare-function mu4e-message-at-point "mu4e")
2239 (declare-function mu4e-root-maildir "mu4e")
2240 (declare-function mu4e-join-paths "mu4e")
2241 (declare-function mu4e-create-maildir-maybe "mu4e")
2242 (declare-function mu4e-ask-maildir "mu4e")
2243 (declare-function mu4e-file-is-note-p "mu4e")
2244 :custom
2245 (mu4e-mu-home (expand-file-name "mu" (or (getenv "XDG_DATA_HOME")
2246 (expand-file-name ".local/share" (getenv "HOME")))))
2247 (mu4e-context-policy 'pick-first)
2248 (mu4e-change-filenames-when-moving t)
2249 (mu4e-attachment-dir "~/desktop/downloads")
2250 :config
2251 ;; No need for get-mail-command - goimapnotify handles syncing via IMAP IDLE
2252 ;; Disable automatic polling - mail arrives instantly via goimapnotify
2253 (setq mu4e-update-interval nil)
2254
2255 ;; Configure msmtp for sending mail
2256 (setq sendmail-program "msmtp"
2257 send-mail-function 'smtpmail-send-it
2258 message-sendmail-f-is-evil t
2259 message-sendmail-extra-arguments '("--read-envelope-from")
2260 message-send-mail-function 'message-send-mail-with-sendmail)
2261
2262 (defun vde-mu4e--extract-body-summary (&optional max-lines)
2263 "Extract a summary from the current mu4e message body.
2264Returns the first MAX-LINES lines (default 10) of the plain text body,
2265with quoted lines and signatures removed."
2266 (let ((max-lines (or max-lines 10))
2267 (msg (mu4e-message-at-point 'noerror)))
2268 (if (not msg)
2269 ""
2270 (let* ((path (mu4e-message-field msg :path))
2271 (body-txt (when (and path (file-exists-p path))
2272 (with-temp-buffer
2273 (insert-file-contents path)
2274 (goto-char (point-min))
2275 ;; Skip headers - find empty line
2276 (when (re-search-forward "^$" nil t)
2277 (forward-line 1)
2278 (buffer-substring-no-properties (point) (point-max)))))))
2279 (if (not body-txt)
2280 ""
2281 ;; Clean up the body
2282 (with-temp-buffer
2283 (insert body-txt)
2284 ;; Try to decode if it looks like quoted-printable
2285 (goto-char (point-min))
2286 (when (re-search-forward "Content-Transfer-Encoding: quoted-printable" nil t)
2287 (goto-char (point-min))
2288 (when (re-search-forward "^$" nil t)
2289 (forward-line 1)
2290 (ignore-errors
2291 (quoted-printable-decode-region (point) (point-max)))))
2292 ;; Remove MIME boundaries and headers
2293 (goto-char (point-min))
2294 (while (re-search-forward "^--.*$\\|^Content-Type:.*$\\|^Content-Transfer-Encoding:.*$" nil t)
2295 (replace-match ""))
2296 ;; Remove HTML tags if present
2297 (goto-char (point-min))
2298 (while (re-search-forward "<[^>]+>" nil t)
2299 (replace-match ""))
2300 ;; Remove quoted lines (starting with >)
2301 (goto-char (point-min))
2302 (while (re-search-forward "^>.*$" nil t)
2303 (replace-match ""))
2304 ;; Remove signature (everything after -- )
2305 (goto-char (point-min))
2306 (when (re-search-forward "^-- $" nil t)
2307 (delete-region (match-beginning 0) (point-max)))
2308 ;; Remove excessive whitespace
2309 (goto-char (point-min))
2310 (while (re-search-forward "^[ \t]*\n\\([ \t]*\n\\)+" nil t)
2311 (replace-match "\n"))
2312 ;; Get first N non-empty lines
2313 (goto-char (point-min))
2314 (let ((lines nil)
2315 (count 0))
2316 (while (and (< count max-lines) (not (eobp)))
2317 (let ((line (string-trim (buffer-substring-no-properties
2318 (line-beginning-position)
2319 (line-end-position)))))
2320 (when (and (> (length line) 0)
2321 (not (string-match-p "^\\s-*$" line)))
2322 (push line lines)
2323 (setq count (1+ count))))
2324 (forward-line 1))
2325 (string-join (nreverse lines) "\n"))))))))
2326
2327 (defun vde-mu4e--body-summary-for-capture ()
2328 "Return email body summary for org-capture template."
2329 (let ((summary (vde-mu4e--extract-body-summary 8)))
2330 (if (string-empty-p summary)
2331 ""
2332 (concat "\n#+begin_quote\n" summary "\n#+end_quote"))))
2333
2334 (defun vde-mu4e--mark-get-copy-target ()
2335 "Ask for a copy target, and propose to create it if it does not exist."
2336 (let* ((target (mu4e-ask-maildir "Copy message to: "))
2337 (target (if (string= (substring target 0 1) "/")
2338 target
2339 (concat "/" target)))
2340 (fulltarget (mu4e-join-paths (mu4e-root-maildir) target)))
2341 (when (mu4e-create-maildir-maybe fulltarget)
2342 target)))
2343
2344 (defun copy-message-to-target(_docid msg target)
2345 (let (
2346 (new_msg_path nil) ;; local variable
2347 (msg_flags (mu4e-message-field msg :flags))
2348 )
2349 ;; 1. target is already determined interactively when executing the mark (:ask-target)
2350
2351 ;; 2. Determine the path for the new file: we use mu4e~draft-message-filename-construct from
2352 ;; mu4e-draft.el to create a new random filename, and append the original's msg_flags
2353 (setq new_msg_path (format "%s/%s/cur/%s" mu4e-maildir target (mu4e~draft-message-filename-construct
2354 (mu4e-flags-to-string msg_flags))))
2355
2356 ;; 3. Copy the message using file system call (copy-file) to new_msg_path:
2357 ;; (See e.g. mu4e-draft.el > mu4e-draft-open > resend)
2358 (copy-file (mu4e-message-field msg :path) new_msg_path)
2359
2360 ;; 4. Add the information to the database (may need to update current search query with 'g' if duplicating to current box. Try also 'V' to toggle the display of duplicates)
2361 (mu4e~proc-add new_msg_path (mu4e~mark-check-target target))
2362 )
2363 )
2364
2365 (defun vde-mu4e--refile (msg)
2366 "Refile function to smartly move `MSG' to a given folder."
2367 (cond
2368 ;; FIXME
2369 ((string= (plist-get (car-safe (mu4e-message-field msg :cc)) :email) "ci_activity@noreply.github.com")
2370 "/icloud/Deleted Messages")
2371 (t
2372 (let ((year (format-time-string "%Y" (mu4e-message-field msg :date))))
2373 (format "/icloud/Archives/%s" year)))))
2374
2375 (setq
2376 mu4e-headers-draft-mark '("D" . "💈")
2377 mu4e-headers-flagged-mark '("F" . "📍")
2378 mu4e-headers-new-mark '("N" . "🔥")
2379 mu4e-headers-passed-mark '("P" . "❯")
2380 mu4e-headers-replied-mark '("R" . "❮")
2381 mu4e-headers-seen-mark '("S" . "☑")
2382 mu4e-headers-trashed-mark '("T" . "💀")
2383 mu4e-headers-attach-mark '("a" . "📎")
2384 mu4e-headers-encrypted-mark '("x" . "🔒")
2385 mu4e-headers-signed-mark '("s" . "🔑")
2386 mu4e-headers-unread-mark '("u" . "⎕")
2387 mu4e-headers-list-mark '("l" . "🔈")
2388 mu4e-headers-personal-mark '("p" . "👨")
2389 mu4e-headers-calendar-mark '("c" . "📅"))
2390
2391 (setopt mu4e-completing-read-function completing-read-function)
2392 (setq mu4e-refile-folder 'vde-mu4e--refile)
2393
2394 ;; === Compose & Reply Settings ===
2395 ;; Don't include own addresses in reply-all
2396 (setq mu4e-compose-reply-ignore-address
2397 '("no-?reply" "vincent@demeester.fr" "vdemeest@redhat.com"))
2398
2399 ;; Better citation format for replies
2400 (setq message-citation-line-format "On %a %d %b %Y at %R, %f wrote:\n"
2401 message-citation-line-function 'message-insert-formatted-citation-line)
2402
2403 ;; Fix improperly formatted sender addresses in replies
2404 (setq rfc2047-quote-decoded-words-containing-tspecials t)
2405
2406 ;; Enable format=flowed for better plain text display on mobile/other clients
2407 (setq mu4e-compose-format-flowed t)
2408
2409 ;; Open compose in a new frame
2410 (setq mu4e-compose-switch 'frame)
2411
2412 (setq mu4e-contexts `( ,(make-mu4e-context
2413 :name "icloud"
2414 :match-func (lambda (msg) (when msg
2415 (string-prefix-p "/icloud" (mu4e-message-field msg :maildir))))
2416 :vars '(
2417 (user-mail-address . "vincent@demeester.fr")
2418 (mu4e-trash-folder . "/icloud/Deleted Messages")
2419 (mu4e-sent-folder . "/icloud/Sent Messages")
2420 (mu4e-draft-folder . "/icloud/Drafts")
2421 ;; (mu4e-get-mail-command . "mbsync icloud")
2422 ))
2423 ,(make-mu4e-context
2424 :name "redhat"
2425 :match-func (lambda (msg) (when msg
2426 (string-prefix-p "/redhat" (mu4e-message-field msg :maildir))))
2427 :vars '(
2428 (user-mail-address . "vdemeest@redhat.com")
2429 (mu4e-drafts-folder . "/redhat/[Gmail]/Drafts")
2430 (mu4e-sent-folder . "/redhat/[Gmail]/Sent Mail")
2431 ;; (mu4e-refile-folder . "/redhat/[Gmail]/All Mail")
2432 (mu4e-trash-folder . "/redhat/[Gmail]/Trash")
2433 ;; (mu4e-get-mail-command . "mbsync redhat")
2434 ))
2435 ))
2436 ;; === Priority & Unread ===
2437 (add-to-list 'mu4e-bookmarks
2438 '(:name "All Inboxes"
2439 :query "maildir:/icloud/Inbox OR maildir:/redhat/Inbox"
2440 :key ?b))
2441
2442 (add-to-list 'mu4e-bookmarks
2443 '(:name "Unread (All)"
2444 :query "flag:unread AND NOT flag:trashed"
2445 :key ?u))
2446
2447 (add-to-list 'mu4e-bookmarks
2448 '(:name "Unread Inboxes"
2449 :query "flag:unread AND (maildir:/icloud/Inbox OR maildir:/redhat/Inbox)"
2450 :key ?U))
2451
2452 (add-to-list 'mu4e-bookmarks
2453 '(:name "Flagged"
2454 :query "flag:flagged AND NOT flag:trashed"
2455 :key ?f))
2456
2457 (add-to-list 'mu4e-bookmarks
2458 '(:name "Today"
2459 :query "date:today..now AND NOT flag:trashed"
2460 :key ?t))
2461
2462 (add-to-list 'mu4e-bookmarks
2463 '(:name "This Week"
2464 :query "date:7d..now AND NOT flag:trashed"
2465 :key ?w))
2466
2467 ;; === Work - RedHat Projects ===
2468 (add-to-list 'mu4e-bookmarks
2469 '(:name "Pipelines/Tekton"
2470 :query "maildir:/redhat/pipelines/* OR maildir:/redhat/tekton/*"
2471 :key ?p))
2472
2473 (add-to-list 'mu4e-bookmarks
2474 '(:name "Konflux"
2475 :query "maildir:/redhat/konflux/*"
2476 :key ?k))
2477
2478 (add-to-list 'mu4e-bookmarks
2479 '(:name "Serverless (Knative)"
2480 :query "maildir:/redhat/serverless/* OR maildir:/redhat/knative/*"
2481 :key ?s))
2482
2483 (add-to-list 'mu4e-bookmarks
2484 '(:name "DevTools Team"
2485 :query "maildir:/redhat/devtools/*"
2486 :key ?d))
2487
2488 (add-to-list 'mu4e-bookmarks
2489 '(:name "Cloud & Insights"
2490 :query "maildir:/redhat/cloud/* OR maildir:/redhat/insights/*"
2491 :key ?c))
2492
2493 ;; === Work - Management & Communication ===
2494 (add-to-list 'mu4e-bookmarks
2495 '(:name "Managers"
2496 :query "maildir:/redhat/managers/*"
2497 :key ?m))
2498
2499 (add-to-list 'mu4e-bookmarks
2500 '(:name "Announcements"
2501 :query "maildir:/redhat/announce/* OR maildir:/redhat/area/*"
2502 :key ?a))
2503
2504 (add-to-list 'mu4e-bookmarks
2505 '(:name "Local (France/Remote)"
2506 :query "maildir:/redhat/local/*"
2507 :key ?l))
2508
2509 ;; === Work - Trackers & Automation ===
2510 (add-to-list 'mu4e-bookmarks
2511 '(:name "Jira Tickets"
2512 :query "maildir:/redhat/_tracker/jira/*"
2513 :key ?j))
2514
2515 (add-to-list 'mu4e-bookmarks
2516 '(:name "All Trackers"
2517 :query "maildir:/redhat/_tracker/*"
2518 :key ?T))
2519
2520 (add-to-list 'mu4e-bookmarks
2521 '(:name "Build Notifications"
2522 :query "maildir:/redhat/_build/*"
2523 :key ?B))
2524
2525 (add-to-list 'mu4e-bookmarks
2526 '(:name "Receipts"
2527 :query "maildir:/icloud/Receipts/*"
2528 :key ?r))
2529
2530 (add-to-list 'mu4e-bookmarks
2531 '(:name "Newsletters"
2532 :query "maildir:/icloud/Newsletters/*"
2533 :key ?n))
2534
2535 (add-to-list 'mu4e-bookmarks
2536 '(:name "House Hunting"
2537 :query "maildir:\"/icloud/house hunting/*\""
2538 :key ?h))
2539
2540 ;; === Sent & Drafts ===
2541 (add-to-list 'mu4e-bookmarks
2542 '(:name "Sent (All)"
2543 :query "maildir:\"/icloud/Sent Messages\" OR maildir:\"/redhat/[Gmail]/Sent Mail\""
2544 :key ?S))
2545
2546 (add-to-list 'mu4e-bookmarks
2547 '(:name "Drafts (All)"
2548 :query "maildir:/icloud/Drafts OR maildir:\"/redhat/[Gmail]/Drafts\""
2549 :key ?D))
2550
2551 ;; === Advanced Queries ===
2552 (add-to-list 'mu4e-bookmarks
2553 '(:name "With Attachments"
2554 :query "flag:attach AND NOT flag:trashed"
2555 :key ?A))
2556
2557 (add-to-list 'mu4e-bookmarks
2558 '(:name "Large Emails (>1MB)"
2559 :query "size:1M..500M AND NOT flag:trashed"
2560 :key ?L))
2561
2562 (add-to-list 'mu4e-bookmarks
2563 '(:name "To Me Directly"
2564 :query "(to:vincent@demeester.fr OR to:vdemeest@redhat.com) AND NOT flag:trashed AND NOT maildir:/_tracker/*"
2565 :key ?M))
2566
2567 ;; === Recent Activity ===
2568 (add-to-list 'mu4e-bookmarks
2569 '(:name "Unread This Week (Work)"
2570 :query "flag:unread AND date:7d..now AND maildir:/redhat/*"
2571 :key ?W))
2572
2573 (add-to-list 'mu4e-bookmarks
2574 '(:name "Unread This Week (Personal)"
2575 :query "flag:unread AND date:7d..now AND maildir:/icloud/*"
2576 :key ?P))
2577
2578 ;; Custom sorting: oldest-first for active folders, newest-first for archives
2579 (declare-function mu4e-search-change-sorting "mu4e")
2580 (declare-function vde/mu4e-set-sort-order-by-bookmark nil)
2581
2582 (defcustom vde/mu4e-oldest-first-bookmarks '(
2583 "maildir:/icloud/Inbox"
2584 "maildir:/redhat/Inbox"
2585 "maildir:/icloud/Inbox OR maildir:/redhat/Inbox" ; All Inboxes
2586 "maildir:/icloud/Drafts"
2587 "maildir:/redhat/[Gmail]/Drafts"
2588 "flag:unread AND NOT flag:trashed" ; Unread (All)
2589 "flag:unread AND (maildir:/icloud/Inbox OR maildir:/redhat/Inbox)" ; Unread Inboxes
2590 "flag:flagged AND NOT flag:trashed" ; Flagged
2591 "(to:vincent@demeester.fr OR to:vdemeest@redhat.com) AND NOT flag:trashed AND NOT maildir:/_tracker/*" ; To Me Directly
2592 )
2593 "Bookmarks/folders to sort oldest-first (ascending).
2594Active folders like INBOX and Drafts show oldest messages first to prioritize
2595pending items by age. All other folders (Sent, Archives) will sort newest-first."
2596 :type '(repeat string)
2597 :group 'mu4e)
2598
2599 (defun vde/mu4e-set-sort-order-by-bookmark (search)
2600 "Set sort order based on bookmark type.
2601Active folders (INBOX, Drafts) sort ascending (oldest first) to prioritize
2602pending items. Archives and Sent folders sort descending (newest first) to
2603show recent activity."
2604 (if (member search vde/mu4e-oldest-first-bookmarks)
2605 (mu4e-search-change-sorting :date 'ascending)
2606 (mu4e-search-change-sorting :date 'descending)))
2607
2608 (add-hook 'mu4e-search-bookmark-hook #'vde/mu4e-set-sort-order-by-bookmark)
2609
2610 ;; Open mu4e links in a new tab
2611 ;; This overrides the default mu4e link handler to open in tab-bar
2612 (defun vde/mu4e-org-open-in-new-tab (link)
2613 "Open mu4e LINK in a new tab-bar tab.
2614LINK is the part after 'mu4e:' in the org link."
2615 (tab-bar-new-tab)
2616 (require 'mu4e-org)
2617 (mu4e-org-open link))
2618
2619 (with-eval-after-load 'ol
2620 (org-link-set-parameters "mu4e"
2621 :follow #'vde/mu4e-org-open-in-new-tab))
2622
2623 ;; imapfilter integration
2624 (defun vde/mu4e-imapfilter-add-rule ()
2625 "Add an imapfilter rule from the current email message.
2626Prompts for rule type (from, domain, subject, header) and category
2627(delete, receipts, newsletters, archive), then appends the rule to
2628the appropriate file in ~/.local/share/imapfilter-rules/"
2629 (interactive)
2630 (unless (mu4e-message-at-point)
2631 (user-error "No message at point"))
2632 (let* ((msg (mu4e-message-at-point))
2633 (from (mu4e-message-field msg :from))
2634 (from-email (when from (plist-get (car from) :email)))
2635 (subject (mu4e-message-field msg :subject))
2636 (rules-dir (expand-file-name "~/.local/share/imapfilter-rules"))
2637
2638 ;; Prompt for rule type
2639 (rule-type (completing-read "Rule type: "
2640 '("from" "domain" "subject" "header")
2641 nil t))
2642
2643 ;; Generate rule pattern based on type
2644 (pattern
2645 (pcase rule-type
2646 ("from" from-email)
2647 ("domain" (when from-email
2648 (concat "@" (replace-regexp-in-string "^.*@" "" from-email))))
2649 ("subject" (read-string "Subject pattern: " subject))
2650 ("header" (let ((header-name (read-string "Header name: "))
2651 (header-value (read-string "Header value: ")))
2652 (format "%s:%s" header-name header-value)))))
2653
2654 ;; Prompt for category
2655 (category (completing-read "Category: "
2656 '("delete" "receipts" "newsletters" "archive")
2657 nil t))
2658
2659 ;; Build rule line
2660 (rule-line (format "%s:%s" rule-type pattern))
2661 (rule-file (expand-file-name (format "%s.txt" category) rules-dir)))
2662
2663 ;; Validate
2664 (unless (file-directory-p rules-dir)
2665 (user-error "Rules directory does not exist: %s" rules-dir))
2666 (unless pattern
2667 (user-error "Could not extract pattern for rule type: %s" rule-type))
2668
2669 ;; Show preview and confirm
2670 (when (y-or-n-p (format "Add rule '%s' to %s.txt? " rule-line category))
2671 ;; Append to file
2672 (with-temp-buffer
2673 (insert rule-line)
2674 (insert "\n")
2675 (append-to-file (point-min) (point-max) rule-file))
2676
2677 (message "Added rule: %s → %s.txt" rule-line category)
2678
2679 ;; Offer to commit and push
2680 (when (y-or-n-p "Commit and push this rule? ")
2681 (let ((default-directory rules-dir))
2682 (shell-command (format "git add %s.txt" category))
2683 (shell-command (format "git commit -m 'Add %s rule: %s'" category rule-line))
2684 (shell-command "git push")
2685 (message "Rule committed and pushed"))))))
2686
2687 (with-eval-after-load "mm-decode"
2688 (add-to-list 'mm-discouraged-alternatives "text/html")
2689 (add-to-list 'mm-discouraged-alternatives "text/richtext")))
2690
2691;; (use-package whisper
2692;; :commands (whisper-run whisper-file)
2693;; :custom
2694;; (whisper-install-whispercpp nil))
2695;; TODO gptel configuration (and *maybe* copilot)
2696
2697(use-package goose
2698 :commands (goose-transient goose-start-session)
2699 :bind (("C-c a G" . goose-transient)))
2700
2701(use-package mcp
2702 :commands (mcp-hub-start-all-server)
2703 :after gptel
2704 :custom (mcp-hub-servers
2705 `(("jira"
2706 :command "/home/vincent/src/github.com/chmouel/jayrah/.venv/bin/jayrah"
2707 :args ("mcp"))
2708 ("github"
2709 :command "github-mcp-server"
2710 :args ("stdio")
2711 :env (:GITHUB_PERSONAL_ACCESS_TOKEN ,(passage-get "github/vdemeester/github-mcp-server")))
2712 ("playwright"
2713 :command "npx @playwright/mcp@latest"
2714 :args ("--executable-path", "/run/current-system/sw/bin/chromium"))))
2715 :config (require 'mcp-hub))
2716
2717(use-package gptel
2718 :commands (gptel gptel-mode)
2719 :init
2720 (declare-function gptel-make-ollama "gptel")
2721 (declare-function gptel-make-openai "gptel")
2722 (declare-function gptel-make-gemini "gptel")
2723 (declare-function gptel-mcp-connect "gptel")
2724 (defvar gptel-mode-map)
2725 :bind (("C-c a g" . gptel))
2726 :hook
2727 (gptel-mode . visual-line-mode)
2728 :bind
2729 (:map gptel-mode-map
2730 ("C-c C-k" . gptel-abort)
2731 ("C-c C-m" . gptel-menu)
2732 ("C-c C-c" . gptel-send))
2733 :custom
2734 (gptel-default-mode #'markdown-mode)
2735 :config
2736 ;; (require 'gptel-curl)
2737 (require 'gptel-gemini)
2738 (require 'gptel-ollama)
2739 (require 'gptel-transient)
2740 (require 'gptel-integrations)
2741 (require 'gptel-rewrite)
2742 (require 'gptel-org)
2743 (require 'gptel-openai)
2744 (require 'gptel-openai-extras)
2745 (require 'gptel-autoloads)
2746 (gptel-mcp-connect)
2747
2748 (setq gptel-model 'gemini-2.5-flash
2749 gptel-backend (gptel-make-gemini "Gemini"
2750 :key (passage-get "ai/gemini/api_key"))
2751 )
2752
2753 (gptel-make-gemini "Gemini Red Hat"
2754 :key (passage-get "redhat/google/osp/vdeemest-api-key"))
2755
2756 (gptel-make-openai "MistralLeChat"
2757 :host "api.mistral.ai/v1"
2758 :endpoint "/chat/completions"
2759 :protocol "https"
2760 :key (passage-get "ai/mistralai/api_key")
2761 :models '("mistral-small"))
2762
2763 (gptel-make-openai "OpenRouter"
2764 :host "openrouter.ai"
2765 :endpoint "/api/v1/chat/completions"
2766 :stream t
2767 :key (passage-get "ai/openroute/api_key")
2768 :models '(cognittivecomputations/dolphin3.0-mistral-24b:free
2769 cognitivecomputations/dolphin3.0-r1-mistral-24b:free
2770 deepseek/deepseek-r1-zero:free
2771 deepseek/deepseek-chat:free
2772 deepseek/deepseek-r1-distill-qwen-32b:free
2773 deepseek/deepseek-r1-distill-llama-70b:free
2774 google/gemini-2.0-flash-lite-preview-02-05:free
2775 google/gemini-2.0-pro-exp-02-05:free
2776 google/gemini-2.5-pro-exp-03-25:free
2777 google/gemma-3-12b-it:free
2778 google/gemma-3-27b-it:free
2779 google/gemma-3-4b-it:free
2780 mistralai/mistral-small-3.1-24b-instruct:free
2781 open-r1/olympiccoder-32b:free
2782 qwen/qwen2.5-vl-3b-instruct:free
2783 qwen/qwen-2.5-coder-32b-instruct:free
2784 qwen/qwq-32b:free
2785 codellama/codellama-70b-instruct
2786 google/gemini-pro
2787 google/palm-2-codechat-bison-32k
2788 meta-llama/codellama-34b-instruct
2789 mistralai/mixtral-8x7b-instruct
2790 openai/gpt-3.5-turbo))
2791 (gptel-make-ollama "Ollama (with metrics)"
2792 :host "192.168.1.23:8000" ; Exporter endpoint for metrics
2793 :stream nil ; Disabled due to gptel streaming issues with Ollama
2794 :models '(;; Tool Calling / OpenCode Support
2795 "llama3.1:8b" ; Best for tool calling
2796 "mistral-nemo:latest" ; Fast tool calling
2797
2798 ;; Coding Models
2799 "qwen2.5-coder:7b" ; Best coding performance
2800 "codestral:latest" ; Large coding model (22B)
2801 "qwen-opencode:latest" ; Custom OpenCode model
2802
2803 ;; Reasoning Models
2804 "deepseek-r1:7b" ; Lightweight reasoning
2805 "phi4-reasoning:latest" ; 14B reasoning
2806
2807 ;; Multimodal
2808 "qwen2.5vl:7b" ; Vision support
2809
2810 ;; Quick Tasks
2811 "phi3.5:3.8b"))
2812 ;; TODO: configure shikoku/kobe ollama instances here
2813 ;; (gptel-make-ollama "Ollama"
2814 ;; :host "localhost:11434"
2815 ;; :stream t
2816 ;; :models '("smollm:latest"
2817 ;; "llama3.1:latest"
2818 ;; "deepseek-r1:latest"
2819 ;; "mistral-small:latest"
2820 ;; "deepseek-r1:7b"
2821 ;; "nomic-embed-text:latest"))
2822 )
2823
2824(use-package acp
2825 :load-path "/home/vincent/src/github.com/xenodium/acp.el/")
2826
2827(use-package agent-shell
2828 :commands (agent-shell agent-shell-toggle)
2829 :load-path "/home/vincent/src/github.com/xenodium/agent-shell/"
2830 :init
2831 (declare-function agent-shell-google-make-authentication "agent-shell")
2832 (declare-function agent-shell-make-environment-variables "agent-shell")
2833 :config
2834 (setq agent-shell-google-authentication
2835 (agent-shell-google-make-authentication :api-key (passage-get "redhat/google/osp/vdeemest-api-key")))
2836 (setq agent-shell-anthropic-claude-environment
2837 (agent-shell-make-environment-variables
2838 "CLAUDE_CODE_USE_VERTEX" "1"
2839 "CLOUD_ML_REGION" "us-east5"
2840 "ANTHROPIC_VERTEX_PROJECT_ID" "itpc-gcp-pnd-pe-eng-claude")))
2841
2842(use-package devdocs
2843 :commands (devdocs-lookup devdocs-install vde/install-devdocs)
2844 :bind (("C-h D" . devdocs-lookup))
2845 :config
2846 (defun vde/install-devdocs ()
2847 "Install the devdocs I am using the most."
2848 (interactive)
2849 (dolist (docset '("bash"
2850 "c"
2851 "click"
2852 "cpp"
2853 "css"
2854 "elisp"
2855 "flask"
2856 "git"
2857 "gnu_make"
2858 "go"
2859 "html"
2860 "htmx"
2861 "http"
2862 "javascript"
2863 "jq"
2864 "jquery"
2865 "kubectl"
2866 "kubernetes"
2867 "lua~5.4"
2868 "nix"
2869 "python~3.13"
2870 "python~3.12"
2871 "requests"
2872 "sqlite"
2873 "terraform"
2874 "werkzeug"
2875 "zig"))
2876 (devdocs-install docset))))
2877
2878(use-package ready-player
2879 :init
2880 (declare-function ready-player-mode "ready-player")
2881 :config
2882 (ready-player-mode +1))
2883
2884(defvar highlight-codetags-keywords
2885 '(("\\<\\(TODO\\|FIXME\\|BUG\\|XXX\\)\\>" 1 font-lock-warning-face prepend)
2886 ("\\<\\(NOTE\\|HACK\\)\\>" 1 font-lock-doc-face prepend)))
2887
2888(define-minor-mode highlight-codetags-local-mode
2889 "Highlight codetags like TODO, FIXME..."
2890 :global nil
2891 (if highlight-codetags-local-mode
2892 (font-lock-add-keywords nil highlight-codetags-keywords)
2893 (font-lock-remove-keywords nil highlight-codetags-keywords))
2894
2895 ;; Fontify the current buffer
2896 (when (bound-and-true-p font-lock-mode)
2897 (if (fboundp 'font-lock-flush)
2898 (font-lock-flush)
2899 (with-no-warnings (font-lock-fontify-buffer)))))
2900
2901(add-hook 'prog-mode-hook #'highlight-codetags-local-mode)
2902
2903(defun vde/wtype-text (text)
2904 "Process TEXT for wtype, handling newlines properly."
2905 (let* ((has-final-newline (string-match-p "\n$" text))
2906 (lines (split-string text "\n"))
2907 (last-idx (1- (length lines))))
2908 (string-join
2909 (cl-loop for line in lines
2910 for i from 0
2911 collect (cond
2912 ;; Last line without final newline
2913 ((and (= i last-idx) (not has-final-newline))
2914 (format "wtype -s 50 \"%s\""
2915 (replace-regexp-in-string "\"" "\\\\\"" line)))
2916 ;; Any other line
2917 (t
2918 (format "wtype -s 50 \"%s\" && wtype -k Return"
2919 (replace-regexp-in-string "\"" "\\\\\"" line)))))
2920 " && ")))
2921
2922(define-minor-mode vde/type-mode
2923 "Minor mode for inserting text via wtype."
2924 :keymap `((,(kbd "C-c C-c") . ,(lambda () (interactive)
2925 (call-process-shell-command
2926 (vde/wtype-text (buffer-string))
2927 nil 0)
2928 (delete-frame)))
2929 (,(kbd "C-c C-k") . ,(lambda () (interactive)
2930 (kill-buffer (current-buffer))))))
2931
2932(defun vde/type ()
2933 "Launch a temporary frame with a clean buffer for typing."
2934 (interactive)
2935 (let ((frame (make-frame '((name . "emacs-float")
2936 (fullscreen . 0)
2937 (undecorated . t)
2938 (width . 70)
2939 (height . 20))))
2940 (buf (get-buffer-create "emacs-float")))
2941 (select-frame frame)
2942 (switch-to-buffer buf)
2943 (with-current-buffer buf
2944 (erase-buffer)
2945 ;; (org-mode)
2946 (markdown-mode) ;; more common ?
2947 (flyspell-mode)
2948 (vde/type-mode)
2949 (setq-local header-line-format
2950 (format " %s to insert text or %s to cancel."
2951 (propertize "C-c C-c" 'face 'help-key-binding)
2952 (propertize "C-c C-k" 'face 'help-key-binding)))
2953 ;; Make the frame more temporary-like
2954 (set-frame-parameter frame 'delete-before-kill-buffer t)
2955 (set-window-dedicated-p (selected-window) t))))
2956
2957(defun vde/agenda ()
2958 "Launch a frame with the org-agenda and the `org-todos-file' buffer."
2959 (interactive)
2960 (let ((frame (make-frame '((name . "emacs-org-agenda")))))
2961 (select-frame frame)
2962 (org-agenda nil "d")
2963 (split-window-horizontally)
2964 (other-window 1)
2965 (find-file org-todos-file)))
2966
2967(defun memoize-remote (key cache orig-fn &rest args)
2968 "Memoize a value if the key is a remote path."
2969 (if (and key
2970 (file-remote-p key))
2971 (if-let ((current (assoc key (symbol-value cache))))
2972 (cdr current)
2973 (let ((current (apply orig-fn args)))
2974 (set cache (cons (cons key current) (symbol-value cache)))
2975 current))
2976 (apply orig-fn args)))
2977;; Memoize current project
2978(defvar project-current-cache nil)
2979(defun memoize-project-current (orig &optional prompt directory)
2980 (memoize-remote (or directory
2981 project-current-directory-override
2982 default-directory)
2983 'project-current-cache orig prompt directory))
2984
2985(advice-add 'project-current :around #'memoize-project-current)
2986
2987;; Memoize magit top level
2988(defvar magit-toplevel-cache nil)
2989(defun memoize-magit-toplevel (orig &optional directory)
2990 (memoize-remote (or directory default-directory)
2991 'magit-toplevel-cache orig directory))
2992(advice-add 'magit-toplevel :around #'memoize-magit-toplevel)
2993
2994;; memoize vc-git-root
2995(defvar vc-git-root-cache nil)
2996(defun memoize-vc-git-root (orig file)
2997 (let ((value (memoize-remote (file-name-directory file) 'vc-git-root-cache orig file)))
2998 ;; sometimes vc-git-root returns nil even when there is a root there
2999 (when (null (cdr (car vc-git-root-cache)))
3000 (setq vc-git-root-cache (cdr vc-git-root-cache)))
3001 value))
3002(advice-add 'vc-git-root :around #'memoize-vc-git-root)
3003
3004;; BIND this
3005(defun mu-date-at-point (date)
3006 "Insert current DATE at point via `completing-read'."
3007 (interactive
3008 (let* ((formats '("%Y%m%d" "%F" "%Y%m%d%H%M" "%Y-%m-%dT%T"))
3009 (vals (mapcar #'format-time-string formats))
3010 (opts
3011 (lambda (string pred action)
3012 (if (eq action 'metadata)
3013 '(metadata (display-sort-function . identity))
3014 (complete-with-action action vals string pred)))))
3015 (list (completing-read "Insert date: " opts nil t))))
3016 (insert date))
3017
3018(provide 'init)
3019
3020;; Local Variables:
3021;; byte-compile-warnings: (not free-vars)
3022;; End:
3023
3024;;; init.el ends here