fix: restore lost work (bookmark mu4e agenda context-tag)

This commit is contained in:
2026-07-17 13:00:58 -04:00
parent bf462da948
commit 4061830979
2 changed files with 1065 additions and 99 deletions

View File

@@ -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

View File

@@ -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)