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