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