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