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