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