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