From 40618309794ce9474973755ca414a7cc505db644 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Fri, 17 Jul 2026 13:00:58 -0400 Subject: [PATCH] fix: restore lost work (bookmark mu4e agenda context-tag) --- doom/.config/doom/README.org | 708 ++++++++++++++++++++++++++++++++--- doom/.config/doom/config.el | 456 +++++++++++++++++++--- 2 files changed, 1065 insertions(+), 99 deletions(-) diff --git a/doom/.config/doom/README.org b/doom/.config/doom/README.org index 854c338..77f8697 100644 --- a/doom/.config/doom/README.org +++ b/doom/.config/doom/README.org @@ -92,6 +92,19 @@ available. You can either set ~doom-theme~ or manually load a theme with the (setq doom-theme 'doom-gruvbox) #+end_src +** Projectile (ExoKortex) + +Only directories with a =.projectile= file are treated as project roots. +No more auto-discovery of random =.git= repos. + +#+begin_src emacs-lisp +(after! projectile + ;; Only .projectile files define a project root + (setq projectile-project-root-files '(".projectile")) + ;; Stop auto-scanning everything — only explicit .projectile markers count + (setq projectile-auto-discover nil)) +#+end_src + ** Bookmark #+begin_src emacs-lisp @@ -107,7 +120,7 @@ To prevent saving a old buffer content after a git branch switch! #+end_src ** Minimize window -My current workflow consist in having the 3-5 files I work on open in vertical split and enlarging one at the time. but sometime I want to see 2 side by side. So I first open all windows with equal size, then collapse all but the 2 I want with this keymap. It's not perfect and I'm thinking about of a buffer based workflow that would give me more screen space. +My current workflow consist in having the 3-5 files I work on open in vertical split and enlarging one at the time. but sometime I want to see 2 side by side. So I first open all windows with equal size, then collapse all but the 2 I want with this keymap. It's not perfect and I'm thinking about a buffer based workflow that would give me more screen space. #+begin_src emacs-lisp (map! :leader @@ -117,6 +130,14 @@ My current workflow consist in having the 3-5 files I work on open in vertical s :desc "Hermes Agent" "h" #'gortium/hermes-vterm)) #+end_src +** Hermes remote shortcut + +#+begin_src emacs-lisp +(map! :leader + (:prefix ("z" . "Personal") + :desc "Hermes Agent" "h" #'gortium/hermes-vterm)) +#+end_src + ** Ultra-scroll #+begin_src emacs-lisp (use-package! ultra-scroll @@ -164,7 +185,7 @@ My current workflow consist in having the 3-5 files I work on open in vertical s ** Fix #+begin_src emacs-lisp -;; Fixes.. +;; Fixes.. idk why or what.. dont ask me (require 'cl-lib) (unless (fboundp 'find-if) (defalias 'find-if #'cl-find-if)) @@ -187,8 +208,18 @@ change ~org-directory~. It must be set before org loads! +org-capture-emails-file "~/ExoKortex/inbox.org" ;; +org-capture-central-project-todo-file '"refile.org" ;; +org-capture-project-todo-file '"refile.org" - org-agenda-files (directory-files-recursively "~/ExoKortex/" "\\.org$") - ) + org-agenda-files + (let ((all (directory-files-recursively "~/ExoKortex/" "\\.org$"))) + (append '("~/ExoKortex/inbox.org" + "~/ExoKortex/projects.org" + "~/ExoKortex/areas.org" + "~/ExoKortex/journal.org" + "~/ExoKortex/telos_thierry_pouplier.org") + (seq-filter + (lambda (f) (string-match-p "/1-Plan/" f)) + all))) +org-agenda-files (seq-uniq org-agenda-files) +) (after! org (setq org-cycle-show-planning-line t) @@ -220,10 +251,19 @@ change ~org-directory~. It must be set before org loads! '(org-level-6 :foreground "#fe8019" :weight normal :height 1.05) ; orange '(org-level-7 :foreground "#cc241d" :weight normal :height 1.02) ; red '(org-level-8 :foreground "#a89984" :weight normal :height 1.00)) ; grey-brown + ;; '(org-level-1 :weight semi-bold :height 1.45) + ;; '(org-level-2 :weight semi-bold :height 1.35) + ;; '(org-level-3 :weight semi-bold :height 1.25) + ;; '(org-level-4 :weight semi-bold :height 1.15) + ;; '(org-level-5 :weight normal :height 1.10) + ;; '(org-level-6 :weight normal :height 1.05) + ;; '(org-level-7 :weight normal :height 1.02) + ;; '(org-level-8 :weight normal :height 1.00)) ;; Clamp all levels beyond 8 to use org-level-8 face (defvar gortium/org-level-8-face '((t :foreground "#a89984" :weight normal :height 1.00))) + ;; '((t :weight normal :height 1.00))) ;; Définir org-level-9 à org-level-20 avec la même face que org-level-8 (cl-loop for lvl from 9 to 20 @@ -310,6 +350,7 @@ change ~org-directory~. It must be set before org loads! org-agenda-skip-deadline-if-done nil org-agenda-skip-scheduled-if-done nil org-agenda-skip-timestamp-if-done nil + org-agenda-sticky t org-journal-enable-agenda-integration t org-log-done 'time org-agenda-span 'day @@ -487,7 +528,7 @@ change ~org-directory~. It must be set before org loads! ) ((org-agenda-tag-filter-preset '("+work"))) ) - ("pp" "Planning" + ("pp" "Planning 1 month" ((agenda "" ((org-agenda-span 'month) (org-agenda-start-day nil) @@ -511,6 +552,54 @@ change ~org-directory~. It must be set before org loads! ) ((org-agenda-tag-filter-preset '("+perso"))) ) + ("pP" "Planning 2 months" + ((agenda "" + ((org-agenda-span 60) + (org-agenda-start-day nil) + (org-agenda-overriding-header "📅 Sheduled Tasks") + (org-agenda-time-grid nil) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags nil)) + ) + + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled TODOs") + (org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled)) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t) + ) + ) + ) + ((org-agenda-tag-filter-preset '("+perso"))) + ) + ("pI" "ImmoFacile" + ((agenda "" + ((org-agenda-span 'month) + (org-agenda-start-day nil) + (org-agenda-overriding-header "📅 Sheduled Tasks") + (org-agenda-time-grid nil) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t)) + ) + + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled TODOs") + (org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled)) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t) + ) + ) + ) + ((org-agenda-tag-filter-preset '("+ImmoFacile"))) + ) ("wP" "Installation Bombardier" ((agenda "" ((org-agenda-span 90) @@ -1266,6 +1355,14 @@ Deactivated for ultra-scroll (after! text-mode (add-hook 'text-mode-hook #'visual-line-mode)) #+end_src +** Rainbow +#+begin_src emacs-lisp +(use-package! rainbow-delimiters + :hook ((emacs-lisp-mode lisp-mode scheme-mode clojure-mode) . rainbow-delimiters-mode)) + +(after! org-src + (add-hook 'org-src-mode-hook #'rainbow-delimiters-mode)) +#+end_src ** Org-phscroll Load org-phscroll after org. This allow horizontal scroll in tables. (No line wrap in table) @@ -1344,7 +1441,7 @@ numbers are disabled. For relative line numbers, set this to ~relative~. (setq display-line-numbers-type 'relative) #+end_src -** Evile-Escape (JK) +** Evil-Escape (JK) JK to escape was not working. Added it back. #+begin_src emacs-lisp @@ -1453,7 +1550,7 @@ I'm a formatting nazi now xD #+end_src ** Age -Allow me to edit encryted age file directly in emacs buffer. +Allow me to edit encrypted age file directly in emacs buffer. #+begin_src emacs-lisp (use-package! age @@ -2086,7 +2183,8 @@ Useful for the user, but also when you have someone over your shoulder trying to #+begin_src emacs-lisp (after! mu4e (setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter) - + (setq mu4e-update-interval 300) + (setq mu4e-headers-auto-update t) (setq mu4e-contexts (list ;; --- CONTEXT: TDNDE --- @@ -2196,6 +2294,195 @@ Cordialement, (evil-define-key 'emacs mu4e-view-mode-map "l" #'mu4e-view-mark-for-label))) #+end_src +** Project mu4e maildir shortcuts + +#+begin_src emacs-lisp +(defconst gortium/project-mu4e-keys '(?I ?S ?D ?T ?A) + "Character keys used for project mu4e maildir shortcuts (uppercase avoids conflicts).") + +(defvar gortium/project-mu4e-bookmarks nil + "Cached project mu4e maildir shortcuts for re-injection when mu4e loads.") + +(defun gortium/inject-project-mu4e-bookmarks () + "Inject project-specific mu4e bookmark shortcuts [PI]/[PS]/[PD]/[PT]/[PA]. +Caches them for re-injection when mu4e loads." + (let* ((proj-root (and (projectile-project-p) (projectile-project-root))) + (proj-tag (when proj-root + (let ((default-directory proj-root)) + (gortium/read-dir-local 'gortium/project-tag)))) + (project-bm '())) + (when proj-tag + (dolist (folder '(("Inbox" . ?I) + ("Sent" . ?S) + ("Drafts" . ?D) + ("Trash" . ?T) + ("Archive" . ?A))) + (push (list :name (format "[%s] %s" proj-tag (car folder)) + :query (format "maildir:/%s.%s" (car folder) proj-tag) + :key (cdr folder)) + project-bm))) + ;; Cache for later re-injection + (setq gortium/project-mu4e-bookmarks project-bm) + ;; Inject now if mu4e is already loaded + (when (boundp 'mu4e-bookmarks) + (setq mu4e-bookmarks + (append + (seq-filter (lambda (bm) + (if (and (listp bm) (keywordp (car-safe bm))) + (let ((key (plist-get bm :key))) + (not (member key gortium/project-mu4e-keys))) + t)) + mu4e-bookmarks) + gortium/project-mu4e-bookmarks)) + ;; Clear caches so new bookmarks appear + (setq mu4e--bookmark-items-cached nil) + ;; Re-render main view if visible + (when (get-buffer "*mu4e-main*") + (with-current-buffer "*mu4e-main*" + (mu4e--main-redraw)))))) + +(add-hook 'projectile-after-switch-project-hook + #'gortium/inject-project-mu4e-bookmarks) +(add-hook 'find-file-hook #'gortium/inject-project-mu4e-bookmarks) +(add-hook 'mu4e-main-mode-hook #'gortium/inject-project-mu4e-bookmarks) +(after! mu4e + (gortium/inject-project-mu4e-bookmarks)) +#+end_src + +* Elfeed (RSS) +#+begin_src emacs-lisp +(after! elfeed + ;; Define where your feed sources are kept (Org-mode file integration) + (setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Assets/rss-feed.org")) + + ;; Set the default search filter (only show unread entries from the last x weeks) + (setq-default elfeed-search-filter "@4-weeks-ago +unread") + + ;; Automatically fetch feed updates over the network when you first open Elfeed + (add-hook! 'elfeed-search-mode-hook #'elfeed-update) + + ;; Optimize connection parameters to prevent lagging and queue timeouts + (setq elfeed-connection-timeout 10) ; Timeout limit for fetching a single feed (seconds) + (setq url-queue-timeout 15) ; Maximum wait time for the network queue + + ;; Set the function used to display entries (pop-to-buffer prevents taking over the whole screen) + (setq elfeed-show-entry-switch #'pop-to-buffer) + +;; A pure MPV launcher (Bypasses buggy EMMS metadata scanners) + (defun gortium/launch-mpv-overlay (url) + "Launch MPV as a floating overlay directly via system process." + (start-process "elfeed-mpv" nil "mpv" + "--quiet" "--really-quiet" + "--geometry=30%x30%-30-50" "--ontop" "--no-border" + "--title=Elfeed MPV" + url)) + + ;; Smart Elfeed Dispatcher + (defun gortium/elfeed-open-dispatcher () + "Play YouTube and Podcasts directly in MPV; open web links in EWW." + (interactive) + (let* ((entry (if (eq major-mode 'elfeed-show-mode) + elfeed-show-entry + (elfeed-search-selected :single))) + (link (elfeed-entry-link entry)) + ;; Extract enclosures (where podcasts store their mp3s) + (enclosures (elfeed-entry-enclosures entry)) + (enclosure-url (when enclosures (car (car enclosures))))) + (cond + ;; Case 1: YouTube -> Direct MPV Overlay + ((and link (string-match-p "youtu\\(?:\\.be\\|be\\.com\\)" link)) + (message "Streaming YouTube video in MPV: %s" link) + (gortium/launch-mpv-overlay link)) + + ;; Case 2: Podcast/Media Enclosure found -> Direct MPV Overlay + (enclosure-url + (message "Streaming podcast enclosure in MPV: %s" enclosure-url) + (gortium/launch-mpv-overlay enclosure-url)) + + ;; Case 3: Standard Web Article (Must start with http) -> EWW + ((and link (string-match-p "^http" link)) + (message "Opening article in EWW (readable): %s" link) + (cond + ((eq major-mode 'elfeed-show-mode) + (eww link)) + (t + (let ((target-window (if (one-window-p) + (split-window-right) + (next-window)))) + (select-window target-window) + (eww link)))) + (add-hook 'eww-after-render-hook #'eww-readable nil t)) + + ;; Case 4: Safety Fallback + (t + (message "No playable media or valid web link found for this entry."))))) + + ;; Bind the dispatcher + (map! :map elfeed-search-mode-map + "M-RET" #'gortium/elfeed-open-dispatcher) + + (map! :map elfeed-show-mode-map + "M-RET" #'gortium/elfeed-open-dispatcher)) + +;; Initialize and configure elfeed-goodies for an enhanced UI layout +(after! elfeed-goodies + ;; Enable the standard elfeed-goodies setup (adds search headers and status lines) + (elfeed-goodies/setup) + + ;; Choose where to split the window when viewing an entry ('bottom, 'left, 'right, 'top) + (setq elfeed-goodies/entry-pane-position 'right) + + ;; Define how much screen space the entry display pane takes (0.5 = 50% of the frame) + (setq elfeed-goodies/entry-pane-size 0.5) + + ;; Display feed tags in their own clean, separate column in the search buffer + (setq elfeed-goodies/show-tags-as-column t) + + ;; Prevent feed source URLs from cluttering up the title line in search results + (setq elfeed-goodies/feed-source-column-width 16)) + +;; Configure elfeed-tube (active when +youtube flag is enabled) +(after! elfeed-tube + ;; Initialize elfeed-tube to fetch YouTube metadata and transcripts + (elfeed-tube-setup) + + ;; Define a quick binding to manually fetch/refresh a transcript if auto-fetch is idle + (map! :map elfeed-show-mode-map + :ne "F" #'elfeed-tube-fetch)) +#+end_src + +* EMMS + +#+begin_Src emacs-lisp +;; Configure EMMS to use MPV for streaming +(after! emms + (require 'emms-setup) + (emms-all) + (require 'emms-source-file) + + ;; Swap VLC out for MPV (which seamlessly leverages yt-dlp) + (setq emms-player-list '(emms-player-mpv)) + + ;; Force EMMS to route HTTP/HTTPS URLs matching YouTube patterns to MPV + (emms-player-set emms-player-mpv 'regex + (rx (or (: "http" (? "s") "://" (* nonl) "youtube.com" (* nonl)) + (: "http" (? "s") "://" (* nonl) "youtu.be" (* nonl)))))) +#+end_src +* EAF +#+begin_src emacs-lisp +(use-package! eaf + :load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework" + :config + (require 'eaf) + + ;; Load the Browser module (for YouTube web streams) + (add-to-list 'load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework/app/browser") + (require 'eaf-browser) + + ;; Load the Video Player module (for local video files) + (add-to-list 'load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework/app/video-player") + (require 'eaf-video-player)) +#+end_src * IRC ** Circe @@ -2260,7 +2547,7 @@ Really useful to be able to have a buffer with notes, and another with the offic * Custom functions -** Custom function refile to datetree +** Refile to datetree #+begin_src emacs-lisp ;;Refile in datetree function @@ -2292,7 +2579,35 @@ If FILE is nil, refile in the current file." (widen))))) #+end_src -** Custom function ediff org fold +** Create a datetree +#+begin_src emacs-lisp +(require 'calendar) + +(defun gortium/org-create-datetree-range () + "Génère une structure datetree Org entre deux dates choisies via le calendrier interactif. +Si la date de début est validée vide, utilise la date d'aujourd'hui." + (interactive) + (let* ((start-time (org-read-date nil t nil "Date de début (Entrée pour aujourd'hui) : ")) + (end-time (org-read-date nil t nil "Date de fin : ")) + (start-decoded (decode-time start-time)) + (end-decoded (decode-time end-time)) + ;; Format attendu par le calendrier Emacs : (mois jour année) + (start-date (list (decoded-time-month start-decoded) + (decoded-time-day start-decoded) + (decoded-time-year start-decoded))) + (end-date (list (decoded-time-month end-decoded) + (decoded-time-day end-decoded) + (decoded-time-year end-decoded))) + (start-abs (calendar-absolute-from-gregorian start-date)) + (end-abs (calendar-absolute-from-gregorian end-date))) + (if (< end-abs start-abs) + (user-error "Erreur : La date de fin doit être après la date de début ! ") + (save-excursion + (cl-loop for abs from start-abs to end-abs + do (org-datetree-find-date-create (calendar-gregorian-from-absolute abs)))) + (message "Datetree généré avec succès !")))) +#+end_src +** Ediff org fold #+begin_src emacs-lisp ;; Open all org fold in ediff @@ -2303,7 +2618,7 @@ If FILE is nil, refile in the current file." (add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer) #+end_src -** Custom function for scheduling +** Scheduling Engine #+begin_src emacs-lisp ;;; ============================================================ @@ -2752,7 +3067,7 @@ and ensure the standard property drawer exists without overwriting existing data #+RESULTS: : gortium/add-ids-to-subtree -** Custom function for maintenance +** Maintenance Engine #+begin_src emacs-lisp (after! org ;; --------------------------------------------------------- @@ -2851,7 +3166,7 @@ and ensure the standard property drawer exists without overwriting existing data (add-hook 'org-after-todo-state-change-hook #'my/org-todo-state-handler) #+end_src -** Custom function for rescheduling task later +** Rescheduling task later #+begin_src emacs-lisp (defun my/org-smart-advance-date () "Jump to next repeater occurrence or add 1 day. @@ -2890,7 +3205,7 @@ Suppresses all logging, notes, and custom odometer hooks." :localleader "L" #'my/org-smart-advance-date)) #+end_src -** Custom function Open link in other frame +** Open link in other frame #+begin_src emacs-lisp ;; Open link in another frame @@ -2906,7 +3221,7 @@ Suppresses all logging, notes, and custom odometer hooks." :n "gF" #'gortium/org-open-link-in-other-frame) #+end_src -** Custom function to open current buffer to new frame +** Open current buffer to new frame #+begin_src emacs-lisp (defvar gortium/main-frame nil @@ -2977,7 +3292,7 @@ Suppresses all logging, notes, and custom odometer hooks." (delete-window old-win))))))) #+end_src -** Custom function for ExoKortex +** ExoKortex System #+begin_src emacs-lisp ;;; ==================================================================== @@ -2987,10 +3302,11 @@ Suppresses all logging, notes, and custom odometer hooks." (defvar gortium/exokortex-root "~/ExoKortex" "The absolute root directory of your ExoKortex.") -(defun gortium/ensure-node-structure (node-dir &optional default-todo) +(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag) "Ensure NODE-DIR conforms to the ExoKortex node blueprint. Creates folders, writes text-layer filters, and handles Git injection. -Returns the absolute path to the core Org file." +When PROJECT-TAG is provided, also creates .projectile and .dir-locals.el +with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)." (let* ((node-dir (expand-file-name node-dir)) (node-name (file-name-nondirectory (directory-file-name node-dir))) ;; Blueprint Paths @@ -3014,39 +3330,84 @@ Returns the absolute path to the core Org file." ;; 3. Build index Org block if absent (unless (file-exists-p org-file) (with-temp-file org-file - (insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n\n* Setup\n- [ ] %s\n" + (insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:\n\n* Setup\nTODO %s\n\n* Bookmarks\n** Example link\n[[file:./%s.org][Example link for %s]]\n\n* Contacts\n** Contact Name\n:PROPERTIES:\n:EMAIL: contact@example.com\n:ROLE: Client / Partner\n:END:\n\n[[mu4e:from:contact@example.com][Correspondence with Contact Name]]\n\n* Tasks\n** TODO First task for %s\n" node-name (format-time-string "[%Y-%m-%d %a]") node-name - (or default-todo "Initialize project context"))))) + (or project-tag "project") + (or default-todo "Initialize project context") + node-name + node-name + node-name)))) - ;; 4. Unified Git Integration Loop + ;; 4. ExoKortex project markers (if project-tag provided) + (when project-tag + (let ((projectile-file (expand-file-name ".projectile" node-dir))) + (unless (file-exists-p projectile-file) + (with-temp-file projectile-file + (insert "")))) + + (let ((dir-locals-file (expand-file-name ".dir-locals.el" node-dir))) + (unless (file-exists-p dir-locals-file) + (with-temp-file dir-locals-file + (prin1 + `((nil . ((gortium/project-tag . ,project-tag) + (gortium/project-agenda-entry . + (("P" ,node-name + ((agenda "" ((org-agenda-span 14) + (org-agenda-overriding-header + ,(format "📅 %s" node-name)))) + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled") + (org-agenda-skip-function + '(org-agenda-skip-entry-if 'scheduled))))) + + ((org-agenda-tag-filter-preset (list ,(concat "+" project-tag)))))))) + ,@(if context-tag + `((gortium/context-tag . ,context-tag)) + nil)))) + (current-buffer)))))) + + ;; 5. Unified Git Integration Loop (let ((default-directory node-dir)) (if (not git-existed-p) (progn (shell-command "git init") - (shell-command "git add .gitignore 1-Plan/ 3-Notes/") + (let ((add-files ".gitignore 1-Plan/ 3-Notes/")) + (when project-tag + (setq add-files (concat add-files " .projectile .dir-locals.el"))) + (shell-command (concat "git add -f " add-files))) (shell-command "git commit -m 'System: Initialize node repository'")) - (shell-command "git add .gitignore 1-Plan/ 3-Notes/"))) + + ;; existing repo: add new marker files if they exist + (let ((add-files ".gitignore 1-Plan/ 3-Notes/")) + (when project-tag + (setq add-files (concat add-files " .projectile .dir-locals.el"))) + (shell-command (concat "git add -f " add-files))))) ;; Pass the target file location back up to the caller - org-file)) - + org-file) ;;; ==================================================================== ;;; Interactive Entry Points ;;; ==================================================================== -(defun gortium/create-node (category node-name) - "Create a brand new standardized PARA Node inside your ExoKortex vault." +(defun gortium/create-node (category node-name tag context-tag) + "Create a brand new standardized PARA Node inside your ExoKortex vault. +TAG is a short slug for agenda/email filtering (e.g. "airbus_vision"). +CONTEXT-TAG is the mu4e/agenda context (e.g. "TDNDE", "Perso")." (interactive (list (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources")) - (read-string "Node Name: "))) + (read-string "Node Name: ") + (read-string "Project tag (slug for agenda/email filtering): ") + (completing-read "Context tag (TDNDE / Perso): " '("TDNDE" "Perso") nil t))) (let* ((category-dir (expand-file-name category gortium/exokortex-root)) (node-dir (expand-file-name node-name category-dir)) - (org-file (gortium/ensure-node-structure node-dir "Initialize project context"))) + (org-file (gortium/ensure-node-structure node-dir + "Initialize project context" tag context-tag))) (find-file org-file) - (message "ExoKortex Node '%s' created in %s." node-name category))) + (message "ExoKortex Node '%s' created in %s with tag '%s' (context: %s)." + node-name category tag context-tag))) (defun gortium/convert-to-node (node-dirs) "Retrofit existing workspace directories into tracked ExoKortex nodes. @@ -3095,7 +3456,7 @@ Otherwise, prompts for a single directory, defaulting to the item under the curs (message "Successfully batch-converted %d directories to ExoKortex nodes!" count)))) #+end_src -** Custom function to refile task to dailies +** Refile task to Roam dailies #+begin_src emacs-lisp (defun gortium/refile-to-today-daily () @@ -3159,35 +3520,52 @@ fallback to today's daily note. Ensures the daily has an Org-roam ID." headline (file-name-nondirectory daily-file))))) #+end_src -** Custom function to create a datetree -#+begin_src emacs-lisp -(require 'calendar) +** Project-Aware Agenda -(defun gortium/org-create-datetree-range () - "Génère une structure datetree Org entre deux dates choisies via le calendrier interactif. -Si la date de début est validée vide, utilise la date d'aujourd'hui." +Reads =.dir-locals.el= from the current Projectile root and injects the +project-specific custom agenda command. Use ~SPC o P~ instead of ~SPC o a~ to +get a context-scoped agenda. + +#+begin_src emacs-lisp +(defun gortium/read-dir-local (var) + "Read variable VAR from .dir-locals.el in `default-directory'. +Returns nil if file doesn't exist or VAR isn't set." + (let ((dir-locals (expand-file-name ".dir-locals.el" default-directory))) + (when (file-exists-p dir-locals) + (with-temp-buffer + (insert-file-contents dir-locals) + (goto-char (point-min)) + (let* ((sexp (ignore-errors (read (current-buffer)))) + (nil-entry (assq nil sexp))) + (when nil-entry + (cdr (assq var (cdr nil-entry))))))))) + +(defun gortium/project-aware-agenda () + "Launch org-agenda scoped to the current Projectile project. +Reads `gortium/project-agenda-entry' from .dir-locals.el and merges +it into `org-agenda-custom-commands'. Restricts `org-agenda-files' +to .org files under the project root." (interactive) - (let* ((start-time (org-read-date nil t nil "Date de début (Entrée pour aujourd'hui) : ")) - (end-time (org-read-date nil t nil "Date de fin : ")) - (start-decoded (decode-time start-time)) - (end-decoded (decode-time end-time)) - ;; Format attendu par le calendrier Emacs : (mois jour année) - (start-date (list (decoded-time-month start-decoded) - (decoded-time-day start-decoded) - (decoded-time-year start-decoded))) - (end-date (list (decoded-time-month end-decoded) - (decoded-time-day end-decoded) - (decoded-time-year end-decoded))) - (start-abs (calendar-absolute-from-gregorian start-date)) - (end-abs (calendar-absolute-from-gregorian end-date))) - (if (< end-abs start-abs) - (user-error "Erreur : La date de fin doit être après la date de début ! ") - (save-excursion - (cl-loop for abs from start-abs to end-abs - do (org-datetree-find-date-create (calendar-gregorian-from-absolute abs)))) - (message "Datetree généré avec succès !")))) + (if (projectile-project-p) + (let* ((proj-root (projectile-project-root)) + (default-directory proj-root) + (agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry)) + (org-agenda-files (directory-files-recursively proj-root "\\.org$")) + (org-agenda-custom-commands + (append org-agenda-custom-commands + (when agenda-entry (list agenda-entry))))) + (org-agenda)) + (org-agenda))) + +(map! :leader + :desc "Project-aware agenda" + "o P" #'gortium/project-aware-agenda) #+end_src -** Custom Emacs-Everywhere for Hyprland + +#+RESULTS: +: gortium/project-aware-agenda + +** Emacs-Everywhere for Hyprland #+begin_src emacs-lisp ;; Like Emacs everywhere, but work in hyprland @@ -3364,3 +3742,219 @@ Need chrome... :( :localleader :desc "Edit Edna rules" "E" #'org-edna-edit)) #+end_src + + + + +** Project-Aware Agenda + +#+begin_src emacs-lisp +;; Remove stale project entries and inject current project's agenda +(defvar gortium/project-agenda-commands nil + "Cached project agenda commands.") + +(defun gortium/inject-project-agenda () + "Inject the current project's custom agenda commands into +`org-agenda-custom-commands', or remove them if no project." + (let* ((proj-root (and (projectile-project-p) (projectile-project-root))) + (project-commands nil)) + (when proj-root + (let ((default-directory proj-root)) + (setq project-commands + (gortium/read-dir-local 'gortium/project-agenda-entry)))) + ;; Remove stale project entries (key "P") + (setq org-agenda-custom-commands + (seq-filter (lambda (entry) + (not (and (stringp (car entry)) + (string= "P" (car entry))))) + org-agenda-custom-commands)) + ;; Add current project commands + (when project-commands + (setq org-agenda-custom-commands + (append org-agenda-custom-commands project-commands))))) + +;; Hook into project switching +(add-hook 'projectile-after-switch-project-hook + #'gortium/inject-project-agenda) +;; Also inject at startup and on file open in a project +(add-hook 'find-file-hook #'gortium/inject-project-agenda) +#+end_src + +** Project Bookmarks + +#+begin_src emacs-lisp +(defvar gortium/project-bookmark-names nil + "Bookmark names currently in the Plan file for the active project.") + +(defun gortium//project-plan-file () + "Return the first .org file under 1-Plan/ at the project root, or nil." + (when-let* ((root (projectile-project-root))) + (car (directory-files (expand-file-name "1-Plan" root) t "\.org\'" t)))) + +(defun gortium//project-tag () + "Read the project tag from .dir-locals.el in the current project." + (when (projectile-project-p) + (let ((default-directory (projectile-project-root))) + (gortium/read-dir-local 'gortium/project-tag)))) + +(defun gortium//read-plan-bookmarks () + "Parse * Bookmarks from the Plan file. +Returns alist ((NAME . URL) ...) where URL is the link target." + (when-let* ((plan-file (gortium//project-plan-file)) + (_ (file-exists-p plan-file))) + (with-temp-buffer + (insert-file-contents plan-file) + (goto-char (point-min)) + (when (re-search-forward "^\* Bookmarks" nil t) + (forward-line 1) + (let ((bookmarks '())) + (while (not (or (eobp) (looking-at "^\* "))) + (cond + ;; ** Name\n[[file:path][Name]] + ((looking-at "^\*\* \(.+\) +\[\[file:\(.+\)\]\[\(.+\)\]\]") + (push (cons (match-string 1) (match-string 2)) bookmarks) + (forward-line 2)) + ;; standalone [[url][name]] + ((looking-at "\[\[\(.+\)\]\[\(.+\)\]\]") + (unless (string-prefix-p "mu4e:" (match-string 1)) + (push (cons (match-string 2) (match-string 1)) bookmarks)) + (forward-line 1)) + (t (forward-line 1)))) + (setq gortium/project-bookmark-names (mapcar #'car bookmarks)) + (nreverse bookmarks)))))) + +(defun gortium//append-bookmark-to-plan (name url) + "Append a bookmark as [[url][name]] under * Bookmarks in the Plan file. +NAME is the display name, URL is the link target (file path or web URL)." + (when-let* ((plan-file (gortium//project-plan-file))) + (with-temp-buffer + (insert-file-contents plan-file) + (goto-char (point-min)) + (if (re-search-forward "^\* Bookmarks" nil t) + (forward-line 1) + (goto-char (point-max)) + (insert " +* Bookmarks +")) + (insert (format "[[%s][%s]] +" url name)) + (write-region (point-min) (point-max) plan-file nil 'silent) + (message "Bookmark added to %s" (file-name-nondirectory plan-file)) + (push name gortium/project-bookmark-names)))) + +(defun gortium//remove-bookmark-from-plan (name) + "Remove a bookmark entry from * Bookmarks in the Plan file." + (when-let* ((plan-file (gortium//project-plan-file))) + (with-temp-buffer + (insert-file-contents plan-file) + (goto-char (point-min)) + (when (re-search-forward (format "^\*\* %s +" (regexp-quote name)) nil t) + (forward-line -1) + (let ((start (point))) + (forward-line 2) + (delete-region start (point))) + (write-region (point-min) (point-max) plan-file nil 'silent) + (message "Bookmark '%s' removed from Plan file" name) + (setq gortium/project-bookmark-names + (delete name gortium/project-bookmark-names)))))) + +(defun gortium/bookmark-set () + "Save a bookmark. Prompts: (g)lobal or (p)roject. +Project bookmarks go to * Bookmarks in the Plan file. +The prompt shows the project tag so you can verify the target." + (interactive) + (let ((choice (read-char-choice "Save: (g)lobal / (p)roject? " '(?g ?p)))) + (pcase choice + (?g (call-interactively 'bookmark-set)) + (?p + (if (projectile-project-p) + (let* ((tag (or (gortium//project-tag) "project")) + (name (read-string (format "[%s] Bookmark name: " tag))) + (file (buffer-file-name))) + (gortium//append-bookmark-to-plan name file) + (message "Project bookmark saved to Plan file.")) + (user-error "Not in a Projectile project")))))) + +(defun gortium/list-project-bookmarks () + "Show project bookmarks in the bookmark menu (with categories). +Temporarily replaces bookmark-alist so the menu shows project bookmarks +in their native categories. On quit restores global bookmarks." + (interactive) + (let* ((bookmarks (gortium//read-plan-bookmarks)) + (names (mapcar #'car bookmarks))) + (if (not names) + (message "No bookmarks found in this project's Plan file.") + (let ((saved-alist (copy-tree bookmark-alist))) + (setq bookmark-alist nil) + (dolist (bm (nreverse (copy-tree bookmarks))) + (bookmark-store (car bm) `((filename . ,(cdr bm))) nil)) + (bookmark-bmenu-list) + (with-current-buffer "*Bookmark List*" + (add-hook 'kill-buffer-hook + (lambda () + (setq bookmark-alist saved-alist) + ;; Write changes back to Plan file + (when-let* ((plan-file (gortium//project-plan-file)) + (entries (bookmark-maybe-sort-alist bookmark-alist))) + (gortium//sync-plan-to-bookmark-file + (mapcar (lambda (e) + (cons (car e) + (alist-get 'filename (cdr e)))) + entries)))) + nil t)))))) + +(defun gortium//sync-plan-to-bookmark-file (bookmarks) + "Write BOOKMARKS alist back to the Plan file under * Bookmarks." + (when-let* ((plan-file (gortium//project-plan-file))) + (with-temp-buffer + (insert-file-contents plan-file) + (when (re-search-forward "^\* Bookmarks" nil t) + (forward-line 1) + (let ((start (point))) + (if (re-search-forward "^\* " nil t) + (forward-line -1) + (goto-char (point-max))) + (delete-region start (point))) + (dolist (bm (nreverse (copy-tree bookmarks))) + (insert (format "[[%s][%s]] +" (cdr bm) (car bm)))) + (write-region (point-min) (point-max) plan-file nil 'silent))))) + +(defun gortium/transfer-to-project () + "Transfer a global bookmark to the current project's Plan file." + (interactive) + (if (projectile-project-p) + (let* ((name (completing-read "Transfer to project: " (bookmark-all-names) nil t)) + (file (bookmark-location name))) + (when (and name file (y-or-n-p (format "Transfer '%s' to project? " name))) + (gortium//append-bookmark-to-plan name file) + (bookmark-delete name) + (bookmark-save) + (message "Transferred '%s' to project %s" name + (or (gortium//project-tag) "")))) + (user-error "Not in a Projectile project"))) + +(defun gortium/transfer-to-global () + "Transfer a project bookmark to the global bookmarks list." + (interactive) + (let* ((bookmarks (gortium//read-plan-bookmarks)) + (names (mapcar #'car bookmarks))) + (if names + (let* ((name (completing-read "Transfer to global: " names nil t)) + (url (cdr (assoc name bookmarks)))) + (when (and name url (y-or-n-p (format "Transfer '%s' to global? " name))) + (bookmark-store name `((filename . ,url)) t) + (bookmark-save) + (gortium//remove-bookmark-from-plan name) + (message "Transferred '%s' to global" name))) + (message "No project bookmarks to transfer.")))) + +;; Bindings +(map! :leader + :desc "Set bookmark (global/project)" + "b m" #'gortium/bookmark-set + :desc "Jump to project bookmark" + "p RET" #'gortium/list-project-bookmarks) +#+end_src \ No newline at end of file diff --git a/doom/.config/doom/config.el b/doom/.config/doom/config.el index 069e466..c962fee 100644 --- a/doom/.config/doom/config.el +++ b/doom/.config/doom/config.el @@ -12,6 +12,12 @@ (setq doom-theme 'doom-gruvbox) +(after! projectile + ;; Only .projectile files define a project root + (setq projectile-project-root-files '(".projectile")) + ;; Stop auto-scanning everything — only explicit .projectile markers count + (setq projectile-auto-discover nil)) + (setq bookmark-save-flag 1) (setq global-auto-revert-mode 1) @@ -22,6 +28,10 @@ (:prefix ("z" . "Personal") :desc "Hermes Agent" "h" #'gortium/hermes-vterm)) +(map! :leader + (:prefix ("z" . "Personal") + :desc "Hermes Agent" "h" #'gortium/hermes-vterm)) + (use-package! ultra-scroll :init (setq scroll-margin 0 ; Required for ultra-scroll @@ -56,7 +66,7 @@ (defvar journal-base-path (file-name-as-directory "~/ExoKortex/2-Areas/Journal")) -;; Fixes.. +;; Fixes.. idk why or what.. dont ask me (require 'cl-lib) (unless (fboundp 'find-if) (defalias 'find-if #'cl-find-if)) @@ -71,8 +81,18 @@ +org-capture-emails-file "~/ExoKortex/inbox.org" ;; +org-capture-central-project-todo-file '"refile.org" ;; +org-capture-project-todo-file '"refile.org" - org-agenda-files (directory-files-recursively "~/ExoKortex/" "\\.org$") - ) + org-agenda-files + (let ((all (directory-files-recursively "~/ExoKortex/" "\\.org$"))) + (append '("~/ExoKortex/inbox.org" + "~/ExoKortex/projects.org" + "~/ExoKortex/areas.org" + "~/ExoKortex/journal.org" + "~/ExoKortex/telos_thierry_pouplier.org") + (seq-filter + (lambda (f) (string-match-p "/1-Plan/" f)) + all))) +org-agenda-files (seq-uniq org-agenda-files) +) (after! org (setq org-cycle-show-planning-line t) @@ -96,10 +116,19 @@ '(org-level-6 :foreground "#fe8019" :weight normal :height 1.05) ; orange '(org-level-7 :foreground "#cc241d" :weight normal :height 1.02) ; red '(org-level-8 :foreground "#a89984" :weight normal :height 1.00)) ; grey-brown + ;; '(org-level-1 :weight semi-bold :height 1.45) + ;; '(org-level-2 :weight semi-bold :height 1.35) + ;; '(org-level-3 :weight semi-bold :height 1.25) + ;; '(org-level-4 :weight semi-bold :height 1.15) + ;; '(org-level-5 :weight normal :height 1.10) + ;; '(org-level-6 :weight normal :height 1.05) + ;; '(org-level-7 :weight normal :height 1.02) + ;; '(org-level-8 :weight normal :height 1.00)) ;; Clamp all levels beyond 8 to use org-level-8 face (defvar gortium/org-level-8-face '((t :foreground "#a89984" :weight normal :height 1.00))) + ;; '((t :weight normal :height 1.00))) ;; Définir org-level-9 à org-level-20 avec la même face que org-level-8 (cl-loop for lvl from 9 to 20 @@ -178,6 +207,7 @@ org-agenda-skip-deadline-if-done nil org-agenda-skip-scheduled-if-done nil org-agenda-skip-timestamp-if-done nil + org-agenda-sticky t org-journal-enable-agenda-integration t org-log-done 'time org-agenda-span 'day @@ -355,7 +385,7 @@ ) ((org-agenda-tag-filter-preset '("+work"))) ) - ("pp" "Planning" + ("pp" "Planning 1 month" ((agenda "" ((org-agenda-span 'month) (org-agenda-start-day nil) @@ -379,6 +409,54 @@ ) ((org-agenda-tag-filter-preset '("+perso"))) ) + ("pP" "Planning 2 months" + ((agenda "" + ((org-agenda-span 60) + (org-agenda-start-day nil) + (org-agenda-overriding-header "📅 Sheduled Tasks") + (org-agenda-time-grid nil) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags nil)) + ) + + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled TODOs") + (org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled)) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t) + ) + ) + ) + ((org-agenda-tag-filter-preset '("+perso"))) + ) + ("pI" "ImmoFacile" + ((agenda "" + ((org-agenda-span 'month) + (org-agenda-start-day nil) + (org-agenda-overriding-header "📅 Sheduled Tasks") + (org-agenda-time-grid nil) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t)) + ) + + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled TODOs") + (org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled)) + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " ") ;; fixed-width TODO + (org-agenda-tags-column -100) ;; right-align tags + (org-agenda-remove-tags t) + ) + ) + ) + ((org-agenda-tag-filter-preset '("+ImmoFacile"))) + ) ("wP" "Installation Bombardier" ((agenda "" ((org-agenda-span 90) @@ -769,6 +847,12 @@ Handles org-clock and context link capture for tasks." (after! text-mode (add-hook 'text-mode-hook #'visual-line-mode)) +(use-package! rainbow-delimiters + :hook ((emacs-lisp-mode lisp-mode scheme-mode clojure-mode) . rainbow-delimiters-mode)) + +(after! org-src + (add-hook 'org-src-mode-hook #'rainbow-delimiters-mode)) + (use-package! org-phscroll :after org) @@ -1257,7 +1341,8 @@ DIFF: (after! mu4e (setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter) - + (setq mu4e-update-interval 300) + (setq mu4e-headers-auto-update t) (setq mu4e-contexts (list ;; --- CONTEXT: TDNDE --- @@ -1366,6 +1451,183 @@ Cordialement, (evil-define-key 'visual mu4e-view-mode-map "l" #'mu4e-view-mark-for-label) (evil-define-key 'emacs mu4e-view-mode-map "l" #'mu4e-view-mark-for-label))) +(defconst gortium/project-mu4e-keys '(?I ?S ?D ?T ?A) + "Character keys used for project mu4e maildir shortcuts (uppercase avoids conflicts).") + +(defvar gortium/project-mu4e-bookmarks nil + "Cached project mu4e maildir shortcuts for re-injection when mu4e loads.") + +(defun gortium/inject-project-mu4e-bookmarks () + "Inject project-specific mu4e bookmark shortcuts [PI]/[PS]/[PD]/[PT]/[PA]. +Caches them for re-injection when mu4e loads." + (let* ((proj-root (and (projectile-project-p) (projectile-project-root))) + (proj-tag (when proj-root + (let ((default-directory proj-root)) + (gortium/read-dir-local 'gortium/project-tag)))) + (project-bm '())) + (when proj-tag + (dolist (folder '(("Inbox" . ?I) + ("Sent" . ?S) + ("Drafts" . ?D) + ("Trash" . ?T) + ("Archive" . ?A))) + (push (list :name (format "[%s] %s" proj-tag (car folder)) + :query (format "maildir:/%s.%s" (car folder) proj-tag) + :key (cdr folder)) + project-bm))) + ;; Cache for later re-injection + (setq gortium/project-mu4e-bookmarks project-bm) + ;; Inject now if mu4e is already loaded + (when (boundp 'mu4e-bookmarks) + (setq mu4e-bookmarks + (append + (seq-filter (lambda (bm) + (if (and (listp bm) (keywordp (car-safe bm))) + (let ((key (plist-get bm :key))) + (not (member key gortium/project-mu4e-keys))) + t)) + mu4e-bookmarks) + gortium/project-mu4e-bookmarks)) + ;; Clear caches so new bookmarks appear + (setq mu4e--bookmark-items-cached nil) + ;; Re-render main view if visible + (when (get-buffer "*mu4e-main*") + (with-current-buffer "*mu4e-main*" + (mu4e--main-redraw)))))) + +(add-hook 'projectile-after-switch-project-hook + #'gortium/inject-project-mu4e-bookmarks) +(add-hook 'find-file-hook #'gortium/inject-project-mu4e-bookmarks) +(add-hook 'mu4e-main-mode-hook #'gortium/inject-project-mu4e-bookmarks) +(after! mu4e + (gortium/inject-project-mu4e-bookmarks)) + +(after! elfeed + ;; Define where your feed sources are kept (Org-mode file integration) + (setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Assets/rss-feed.org")) + + ;; Set the default search filter (only show unread entries from the last x weeks) + (setq-default elfeed-search-filter "@4-weeks-ago +unread") + + ;; Automatically fetch feed updates over the network when you first open Elfeed + (add-hook! 'elfeed-search-mode-hook #'elfeed-update) + + ;; Optimize connection parameters to prevent lagging and queue timeouts + (setq elfeed-connection-timeout 10) ; Timeout limit for fetching a single feed (seconds) + (setq url-queue-timeout 15) ; Maximum wait time for the network queue + + ;; Set the function used to display entries (pop-to-buffer prevents taking over the whole screen) + (setq elfeed-show-entry-switch #'pop-to-buffer) + +;; A pure MPV launcher (Bypasses buggy EMMS metadata scanners) + (defun gortium/launch-mpv-overlay (url) + "Launch MPV as a floating overlay directly via system process." + (start-process "elfeed-mpv" nil "mpv" + "--quiet" "--really-quiet" + "--geometry=30%x30%-30-50" "--ontop" "--no-border" + "--title=Elfeed MPV" + url)) + + ;; Smart Elfeed Dispatcher + (defun gortium/elfeed-open-dispatcher () + "Play YouTube and Podcasts directly in MPV; open web links in EWW." + (interactive) + (let* ((entry (if (eq major-mode 'elfeed-show-mode) + elfeed-show-entry + (elfeed-search-selected :single))) + (link (elfeed-entry-link entry)) + ;; Extract enclosures (where podcasts store their mp3s) + (enclosures (elfeed-entry-enclosures entry)) + (enclosure-url (when enclosures (car (car enclosures))))) + (cond + ;; Case 1: YouTube -> Direct MPV Overlay + ((and link (string-match-p "youtu\\(?:\\.be\\|be\\.com\\)" link)) + (message "Streaming YouTube video in MPV: %s" link) + (gortium/launch-mpv-overlay link)) + + ;; Case 2: Podcast/Media Enclosure found -> Direct MPV Overlay + (enclosure-url + (message "Streaming podcast enclosure in MPV: %s" enclosure-url) + (gortium/launch-mpv-overlay enclosure-url)) + + ;; Case 3: Standard Web Article (Must start with http) -> EWW + ((and link (string-match-p "^http" link)) + (message "Opening article in EWW (readable): %s" link) + (cond + ((eq major-mode 'elfeed-show-mode) + (eww link)) + (t + (let ((target-window (if (one-window-p) + (split-window-right) + (next-window)))) + (select-window target-window) + (eww link)))) + (add-hook 'eww-after-render-hook #'eww-readable nil t)) + + ;; Case 4: Safety Fallback + (t + (message "No playable media or valid web link found for this entry."))))) + + ;; Bind the dispatcher + (map! :map elfeed-search-mode-map + "M-RET" #'gortium/elfeed-open-dispatcher) + + (map! :map elfeed-show-mode-map + "M-RET" #'gortium/elfeed-open-dispatcher)) + +;; Initialize and configure elfeed-goodies for an enhanced UI layout +(after! elfeed-goodies + ;; Enable the standard elfeed-goodies setup (adds search headers and status lines) + (elfeed-goodies/setup) + + ;; Choose where to split the window when viewing an entry ('bottom, 'left, 'right, 'top) + (setq elfeed-goodies/entry-pane-position 'right) + + ;; Define how much screen space the entry display pane takes (0.5 = 50% of the frame) + (setq elfeed-goodies/entry-pane-size 0.5) + + ;; Display feed tags in their own clean, separate column in the search buffer + (setq elfeed-goodies/show-tags-as-column t) + + ;; Prevent feed source URLs from cluttering up the title line in search results + (setq elfeed-goodies/feed-source-column-width 16)) + +;; Configure elfeed-tube (active when +youtube flag is enabled) +(after! elfeed-tube + ;; Initialize elfeed-tube to fetch YouTube metadata and transcripts + (elfeed-tube-setup) + + ;; Define a quick binding to manually fetch/refresh a transcript if auto-fetch is idle + (map! :map elfeed-show-mode-map + :ne "F" #'elfeed-tube-fetch)) + +;; Configure EMMS to use MPV for streaming +(after! emms + (require 'emms-setup) + (emms-all) + (require 'emms-source-file) + + ;; Swap VLC out for MPV (which seamlessly leverages yt-dlp) + (setq emms-player-list '(emms-player-mpv)) + + ;; Force EMMS to route HTTP/HTTPS URLs matching YouTube patterns to MPV + (emms-player-set emms-player-mpv 'regex + (rx (or (: "http" (? "s") "://" (* nonl) "youtube.com" (* nonl)) + (: "http" (? "s") "://" (* nonl) "youtu.be" (* nonl)))))) + +(use-package! eaf + :load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework" + :config + (require 'eaf) + + ;; Load the Browser module (for YouTube web streams) + (add-to-list 'load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework/app/browser") + (require 'eaf-browser) + + ;; Load the Video Player module (for local video files) + (add-to-list 'load-path "~/.config/emacs/.local/straight/repos/emacs-application-framework/app/video-player") + (require 'eaf-video-player)) + (defun gortium-circe-nickserv-password (server) (auth-source-passage-get 'secret "irc")) @@ -1424,6 +1686,32 @@ If FILE is nil, refile in the current file." (org-paste-subtree 4) (widen))))) +(require 'calendar) + +(defun gortium/org-create-datetree-range () + "Génère une structure datetree Org entre deux dates choisies via le calendrier interactif. +Si la date de début est validée vide, utilise la date d'aujourd'hui." + (interactive) + (let* ((start-time (org-read-date nil t nil "Date de début (Entrée pour aujourd'hui) : ")) + (end-time (org-read-date nil t nil "Date de fin : ")) + (start-decoded (decode-time start-time)) + (end-decoded (decode-time end-time)) + ;; Format attendu par le calendrier Emacs : (mois jour année) + (start-date (list (decoded-time-month start-decoded) + (decoded-time-day start-decoded) + (decoded-time-year start-decoded))) + (end-date (list (decoded-time-month end-decoded) + (decoded-time-day end-decoded) + (decoded-time-year end-decoded))) + (start-abs (calendar-absolute-from-gregorian start-date)) + (end-abs (calendar-absolute-from-gregorian end-date))) + (if (< end-abs start-abs) + (user-error "Erreur : La date de fin doit être après la date de début ! ") + (save-excursion + (cl-loop for abs from start-abs to end-abs + do (org-datetree-find-date-create (calendar-gregorian-from-absolute abs)))) + (message "Datetree généré avec succès !")))) + ;; Open all org fold in ediff (defun org-ediff-prepare-buffer () (when (memq major-mode '(org-mode emacs-lisp-mode)) @@ -2092,10 +2380,11 @@ Suppresses all logging, notes, and custom odometer hooks." (defvar gortium/exokortex-root "~/ExoKortex" "The absolute root directory of your ExoKortex.") -(defun gortium/ensure-node-structure (node-dir &optional default-todo) +(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag) "Ensure NODE-DIR conforms to the ExoKortex node blueprint. Creates folders, writes text-layer filters, and handles Git injection. -Returns the absolute path to the core Org file." +When PROJECT-TAG is provided, also creates .projectile and .dir-locals.el +with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)." (let* ((node-dir (expand-file-name node-dir)) (node-name (file-name-nondirectory (directory-file-name node-dir))) ;; Blueprint Paths @@ -2119,39 +2408,84 @@ Returns the absolute path to the core Org file." ;; 3. Build index Org block if absent (unless (file-exists-p org-file) (with-temp-file org-file - (insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n\n* Setup\n- [ ] %s\n" + (insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:\n\n* Setup\nTODO %s\n\n* Bookmarks\n** Example link\n[[file:./%s.org][Example link for %s]]\n\n* Contacts\n** Contact Name\n:PROPERTIES:\n:EMAIL: contact@example.com\n:ROLE: Client / Partner\n:END:\n\n[[mu4e:from:contact@example.com][Correspondence with Contact Name]]\n\n* Tasks\n** TODO First task for %s\n" node-name (format-time-string "[%Y-%m-%d %a]") node-name - (or default-todo "Initialize project context"))))) + (or project-tag "project") + (or default-todo "Initialize project context") + node-name + node-name + node-name)))) - ;; 4. Unified Git Integration Loop + ;; 4. ExoKortex project markers (if project-tag provided) + (when project-tag + (let ((projectile-file (expand-file-name ".projectile" node-dir))) + (unless (file-exists-p projectile-file) + (with-temp-file projectile-file + (insert "")))) + + (let ((dir-locals-file (expand-file-name ".dir-locals.el" node-dir))) + (unless (file-exists-p dir-locals-file) + (with-temp-file dir-locals-file + (prin1 + `((nil . ((gortium/project-tag . ,project-tag) + (gortium/project-agenda-entry . + (("P" ,node-name + ((agenda "" ((org-agenda-span 14) + (org-agenda-overriding-header + ,(format "📅 %s" node-name)))) + (todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA" + ((org-agenda-overriding-header "📋 Unscheduled") + (org-agenda-skip-function + '(org-agenda-skip-entry-if 'scheduled))))) + + ((org-agenda-tag-filter-preset (list ,(concat "+" project-tag)))))))) + ,@(if context-tag + `((gortium/context-tag . ,context-tag)) + nil)))) + (current-buffer)))))) + + ;; 5. Unified Git Integration Loop (let ((default-directory node-dir)) (if (not git-existed-p) (progn (shell-command "git init") - (shell-command "git add .gitignore 1-Plan/ 3-Notes/") + (let ((add-files ".gitignore 1-Plan/ 3-Notes/")) + (when project-tag + (setq add-files (concat add-files " .projectile .dir-locals.el"))) + (shell-command (concat "git add -f " add-files))) (shell-command "git commit -m 'System: Initialize node repository'")) - (shell-command "git add .gitignore 1-Plan/ 3-Notes/"))) + + ;; existing repo: add new marker files if they exist + (let ((add-files ".gitignore 1-Plan/ 3-Notes/")) + (when project-tag + (setq add-files (concat add-files " .projectile .dir-locals.el"))) + (shell-command (concat "git add -f " add-files))))) ;; Pass the target file location back up to the caller - org-file)) - + org-file) ;;; ==================================================================== ;;; Interactive Entry Points ;;; ==================================================================== -(defun gortium/create-node (category node-name) - "Create a brand new standardized PARA Node inside your ExoKortex vault." +(defun gortium/create-node (category node-name tag context-tag) + "Create a brand new standardized PARA Node inside your ExoKortex vault. +TAG is a short slug for agenda/email filtering (e.g. "airbus_vision"). +CONTEXT-TAG is the mu4e/agenda context (e.g. "TDNDE", "Perso")." (interactive (list (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources")) - (read-string "Node Name: "))) + (read-string "Node Name: ") + (read-string "Project tag (slug for agenda/email filtering): ") + (completing-read "Context tag (TDNDE / Perso): " '("TDNDE" "Perso") nil t))) (let* ((category-dir (expand-file-name category gortium/exokortex-root)) (node-dir (expand-file-name node-name category-dir)) - (org-file (gortium/ensure-node-structure node-dir "Initialize project context"))) + (org-file (gortium/ensure-node-structure node-dir + "Initialize project context" tag context-tag))) (find-file org-file) - (message "ExoKortex Node '%s' created in %s." node-name category))) + (message "ExoKortex Node '%s' created in %s with tag '%s' (context: %s)." + node-name category tag context-tag))) (defun gortium/convert-to-node (node-dirs) "Retrofit existing workspace directories into tracked ExoKortex nodes. @@ -2258,31 +2592,39 @@ fallback to today's daily note. Ensures the daily has an Org-roam ID." headline (file-name-nondirectory daily-file))))) -(require 'calendar) +(defun gortium/read-dir-local (var) + "Read variable VAR from .dir-locals.el in `default-directory'. +Returns nil if file doesn't exist or VAR isn't set." + (let ((dir-locals (expand-file-name ".dir-locals.el" default-directory))) + (when (file-exists-p dir-locals) + (with-temp-buffer + (insert-file-contents dir-locals) + (goto-char (point-min)) + (let* ((sexp (ignore-errors (read (current-buffer)))) + (nil-entry (assq nil sexp))) + (when nil-entry + (cdr (assq var (cdr nil-entry))))))))) -(defun gortium/org-create-datetree-range () - "Génère une structure datetree Org entre deux dates choisies via le calendrier interactif. -Si la date de début est validée vide, utilise la date d'aujourd'hui." +(defun gortium/project-aware-agenda () + "Launch org-agenda scoped to the current Projectile project. +Reads `gortium/project-agenda-entry' from .dir-locals.el and merges +it into `org-agenda-custom-commands'. Restricts `org-agenda-files' +to .org files under the project root." (interactive) - (let* ((start-time (org-read-date nil t nil "Date de début (Entrée pour aujourd'hui) : ")) - (end-time (org-read-date nil t nil "Date de fin : ")) - (start-decoded (decode-time start-time)) - (end-decoded (decode-time end-time)) - ;; Format attendu par le calendrier Emacs : (mois jour année) - (start-date (list (decoded-time-month start-decoded) - (decoded-time-day start-decoded) - (decoded-time-year start-decoded))) - (end-date (list (decoded-time-month end-decoded) - (decoded-time-day end-decoded) - (decoded-time-year end-decoded))) - (start-abs (calendar-absolute-from-gregorian start-date)) - (end-abs (calendar-absolute-from-gregorian end-date))) - (if (< end-abs start-abs) - (user-error "Erreur : La date de fin doit être après la date de début ! ") - (save-excursion - (cl-loop for abs from start-abs to end-abs - do (org-datetree-find-date-create (calendar-gregorian-from-absolute abs)))) - (message "Datetree généré avec succès !")))) + (if (projectile-project-p) + (let* ((proj-root (projectile-project-root)) + (default-directory proj-root) + (agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry)) + (org-agenda-files (directory-files-recursively proj-root "\\.org$")) + (org-agenda-custom-commands + (append org-agenda-custom-commands + (when agenda-entry (list agenda-entry))))) + (org-agenda)) + (org-agenda))) + +(map! :leader + :desc "Project-aware agenda" + "o P" #'gortium/project-aware-agenda) ;; Like Emacs everywhere, but work in hyprland (defun thanos/wtype-text (text) @@ -2337,3 +2679,33 @@ Si la date de début est validée vide, utilise la date d'aujourd'hui." ;; Make the frame more temporary-like (set-frame-parameter frame 'delete-before-kill-buffer t) (set-window-dedicated-p (selected-window) t)))) + +;; Remove stale project entries and inject current project's agenda +(defvar gortium/project-agenda-commands nil + "Cached project agenda commands.") + +(defun gortium/inject-project-agenda () + "Inject the current project's custom agenda commands into +`org-agenda-custom-commands', or remove them if no project." + (let* ((proj-root (and (projectile-project-p) (projectile-project-root))) + (project-commands nil)) + (when proj-root + (let ((default-directory proj-root)) + (setq project-commands + (gortium/read-dir-local 'gortium/project-agenda-entry)))) + ;; Remove stale project entries (key "P") + (setq org-agenda-custom-commands + (seq-filter (lambda (entry) + (not (and (stringp (car entry)) + (string= "P" (car entry))))) + org-agenda-custom-commands)) + ;; Add current project commands + (when project-commands + (setq org-agenda-custom-commands + (append org-agenda-custom-commands project-commands))))) + +;; Hook into project switching +(add-hook 'projectile-after-switch-project-hook + #'gortium/inject-project-agenda) +;; Also inject at startup and on file open in a project +(add-hook 'find-file-hook #'gortium/inject-project-agenda)