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