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