#+TITLE: Gortium Doom Emacs Configuration
#+PROPERTY: header-args:emacs-lisp :tangle config.el

* Doom Emacs

Whenever you reconfigure a package, make sure to wrap your config in an
~after!~ block, otherwise Doom's defaults may override your settings. E.g.

#+BEGIN_SRC emacs-lisp :tangle no
  (after! PACKAGE
    (setq x y))
#+END_SRC

The exceptions to this rule:

  - Setting file/directory variables (like ~org-directory~)
  - Setting variables which explicitly tell you to set them before their
    package is loaded (see ~C-h v VARIABLE~ to look up their documentation).
  - Setting doom variables (which start with ~doom-~ or ~+~).

Here are some additional functions/macros that will help you configure Doom.

- ~load!~ for loading external =*.el= files relative to this one
- ~use-package!~ for configuring packages
- ~after!~ for running code after a package has loaded
- ~add-load-path!~ for adding directories to the ~load-path~, relative to
  this file. Emacs searches the ~load-path~ when you load packages with
  ~require~ or ~use-package~.
- ~map!~ for binding new keys

To get information about any of these functions/macros, move the cursor over
the highlighted symbol at press 'K' (non-evil users must press ~C-c c k~).
This will open documentation for it, including demos of how they are used.
Alternatively, use ~C-h o~ to look up a symbol (functions, variables, faces,
etc).

You can also try ~gd~ (or ~C-c c d~) to jump to their definition and see how
they are implemented.

** Config File Header

#+begin_src emacs-lisp
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
#+end_src

** User Info

Place your private configurations. It is optional.

#+begin_src emacs-lisp
(setq user-full-name "Thierry Pouplier"
      user-mail-address "tpouplier@tdnde.com")
#+end_src

#+RESULTS:
: tpouplier@tdnde.com

** Fonts

 Doom exposes five (optional) variables for controlling fonts in Doom:
 - ~doom-font~ -- the primary font to use
 - ~doom-variable-pitch-font~ -- a non-monospace font (where applicable)
 - ~doom-big-font~ -- used for ~doom-big-font-mode~; use this for
   presentations or streaming.
 - ~doom-symbol-font~ -- for symbols
 - ~doom-serif-font~ -- for the ~fixed-pitch-serif~ face

 See ~C-h v doom-font~ for documentation and more examples of what they
 accept. For example:

#+begin_src emacs-lisp
(setq doom-font (font-spec :family "JetBrainsMono Nerd Font" :size 16)
      doom-symbol-font (font-spec :family "JetBrainsMono Nerd Font" :size 20))

;; BIGGER FONT MODE
(after! doom-big-font-mode
  (setq doom-big-font-increment 8))
#+end_src

** Theme

If you or Emacs can't find your font, use ~M-x describe-font~ to look them
up, ~M-x eval-region~ to execute elisp code, and ~M-x doom/reload-font~ to
refresh your font settings. If Emacs still can't find your font, it likely
wasn't installed correctly. Font issues are rarely Doom issues!

There are two ways to load a theme. Both assume the theme is installed and
available. You can either set ~doom-theme~ or manually load a theme with the
~load-theme~ function. This is the default:

#+begin_src emacs-lisp
(setq doom-theme 'doom-gruvbox)
#+end_src

** Bookmark

#+begin_src emacs-lisp
(setq bookmark-save-flag 1)
#+end_src

** Buffer auto reload
Detect changes from disk and reload buffers
To prevent saving a old buffer content after a git branch switch!

#+begin_src emacs-lisp
(setq global-auto-revert-mode 1)
#+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.

#+begin_src emacs-lisp
(map! :leader
      (:prefix ("w" . "window")
       :desc "Minimize window" "O" #'minimize-window))
#+end_src

** Ultra-scroll
#+begin_src emacs-lisp
(use-package! ultra-scroll
  :init
  (setq scroll-margin 0                          ; Required for ultra-scroll
        scroll-conservatively 101                ; Prevents jumping to center
        ultra-scroll-acceleration nil            ; Essential for Miryoku keys
        ultra-scroll-mouse-scale 0.1             ; Lower for higher precision
        pixel-scroll-precision-large-scroll-height 20)
  :config
  (ultra-scroll-mode 1))
#+end_src

** Super-Save

#+begin_src emacs-lisp
;; Super- auto -save
(use-package! super-save
  :config
  (setq super-save-auto-save-when-idle t)
  (setq super-save-all-buffers t)
  (setq super-save-triggers
        '(switch-to-buffer other-window windmove-up windmove-down
          windmove-left windmove-right next-buffer previous-buffer
          evil-window-delete evil-window-vsplit evil-window-spliti
          evil-prev-buffer evil-next-buffer))
  (super-save-mode +1))
#+end_src

** Fix

#+begin_src emacs-lisp
;; Fixes..
(require 'cl-lib)
(unless (fboundp 'find-if)
  (defalias 'find-if #'cl-find-if))
(unless (fboundp 'getf)
  (defalias 'getf #'cl-getf))
#+end_src

* Org Mode

If you use ~org~ and don't want your org files in the default location below,
change ~org-directory~. It must be set before org loads!

** Static Paths

#+begin_src emacs-lisp
(setq
 
 org-directory "~/ExoKortex/"
 +org-capture-todo-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
 +org-capture-notes-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
 +org-capture-journal-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
 +org-capture-emails-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
 ;; +org-capture-central-project-todo-file '"refile.org"
 ;; +org-capture-project-todo-file '"refile.org"
 org-agenda-files (directory-files-recursively "~/ExoKortex/" "\\.org$")
 )
#+end_src

** Refiling

#+begin_src emacs-lisp
(after! org
  (setq org-refile-targets
        '((nil :maxlevel . 6)            ;; Current buffer
          (org-agenda-files :maxlevel . 6)))  ;; All agenda files
)
#+end_src

** Headers

#+begin_src emacs-lisp
(after! org
  ;; Header size
  (custom-set-faces!
    '(org-document-title :height 2.0 :weight bold)
    '(org-level-1 :foreground "#b8bb26" :weight semi-bold :height 1.45) ; green
    '(org-level-2 :foreground "#83a598" :weight semi-bold :height 1.35) ; blue
    '(org-level-3 :foreground "#d3869b" :weight semi-bold :height 1.25) ; purple
    '(org-level-4 :foreground "#8ec07c" :weight semi-bold :height 1.15) ; aqua
    '(org-level-5 :foreground "#fabd2f" :weight normal :height 1.10)    ; yellow
    '(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

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

  ;; 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
           do (eval
               `(defface ,(intern (format "org-level-%d" lvl))
                  ',gortium/org-level-8-face
                  ,(format "Face pour org heading level %d (cloné du niveau 8)." lvl)
                  :group 'org-faces)))

  ;; Ajout des nouvelles faces dans org-level-faces
  (setq org-level-faces
        (append org-level-faces
                (mapcar (lambda (lvl) (intern (format "org-level-%d" lvl)))
                        (number-sequence 9 20))))

  ;; Mise à jour du nombre total de niveaux
  (setq org-n-level-faces (length org-level-faces))
)
#+end_src

** Clock-in

#+begin_src emacs-lisp
(after! org
  ;; Automatically clock in when switching to STRT
  (defun gortium/org-clock-in-if-starting ()
    "Clock in when task is switched to STRT."
    (when (and (string= org-state "STRT")
               (not (bound-and-true-p org-clock-resolving-clocks-due-to-idleness)))
      (org-clock-in)))

  ;; Automatically clock out when switching to DONE
  (defun gortium/org-clock-out-if-not-starting ()
    "Clock out if task was in STRT and is being changed to anything else."
    (when (and (string= org-last-state "STRT")
               (not (string= org-state "STRT")))
      (when (org-clocking-p)
        (org-clock-out))))

  ;; Hook both functions into todo state change
  (add-hook 'org-after-todo-state-change-hook #'gortium/org-clock-in-if-starting)
  (add-hook 'org-after-todo-state-change-hook #'gortium/org-clock-out-if-not-starting)

  ;; Enable Org Clocking
  (setq org-clock-persist 'history) ;; Save clock history across sessions
  (add-hook 'org-mode-hook 'org-clock-load) ; Load persisted clocks
  (setq org-clock-persist 'history)         ; Or 't for full persistence
  (setq org-clock-in-resume t)              ; Resume clock when restarting the task

  ;; Save clock data and state changes when exiting Emacs
  (setq org-clock-out-remove-zero-time-clocks t)
  (setq org-clock-in-resume t) ;; Resume clocking when moving between tasks

  ;; Always clock out when exiting Emacs
  ;; (add-hook 'kill-emacs-hook #'(lambda () (when (org-clocking-p) (org-clock-out))))

  ;; Mode-line clock display
  (setq org-clock-mode-line-total 'auto) ;; show today's total
  (org-clock-display) ;; activate the mode-line display

  (custom-set-faces!
    '(org-mode-line-clock
      :foreground "gold"
      :weight bold))
)
#+end_src

** Org Agenda

#+begin_src emacs-lisp
(after! org
  (setq org-columns-default-format "%65ITEM %4TODO %3PRIORITY %ASSIGNEE %5EFFORT{:} %5CLOCKSUM %16SCHEDULED %16DEADLINE %10TAGS")

  (setq org-global-properties
        '(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
          ("ASSIGNEE_ALL" . "Thierry.P Cath.F Martin.K Michel.B Gabriel.C Silvie.L George.G Miguel.A Réjean.S Dominique Adnane Hatim.K Marc-Antoine.P")))

  (setq org-stuck-projects
        '("TODO=\"PROJ\"" ("NEXT") nil ""))
  
  (setq org-agenda-exporter-settings '((ps-print-color-p 'nil)))
  (setq
   org-agenda-follow-mode nil
   org-agenda-skip-deadline-if-done nil
   org-agenda-skip-scheduled-if-done nil
   org-agenda-skip-timestamp-if-done nil
   org-journal-enable-agenda-integration t
   org-log-done 'time
   org-agenda-span 'day
   org-log-into-drawer t
   org-log-redeadline 'time
   org-log-reschedule 'time
   org-todo-repeat-to-state '"LOOP"
   org-todo-keywords
   '(
     (sequence
      ;; Tasks
      "TODO(t)" "STRT(s)" "LOOP(r)" "NEXT(n)" "DELG(g)"
      "WAIT(w@/!)" "HOLD(h@/!)" "IDEA(i)"

      ;; Framework
      "EPIC(e)" "AREA(a)" "PROJ(p)"

      ;; Done / Cancelled
      "|" "DONE(d!)" "CNCL(c@/!)")

     ;; Checkboxes
     (sequence "[ ](T)" "[-](S)" "|" "[X](D)")

     ;; Special indicators
     (sequence "[!](F)" "[?](W)" "|" "[i](I)")

     ;; Binary questions
     (sequence "Y/N(M)" "|" "YES(Y)" "NOP(N)")
     )
   )
  ;; Not there yet.. Need better integration with org-modern and gruvbox theme
  ;; %a : org-store-link
  ;; %i : insert from selection
  ;; %? : cursor position at the end
  ;; %u : unactive date
  ;; %t : ative date
  ;; %:subject : subject of the message
  ;; %:from : The full sender string including name and address
  ;; %:fromname : The display name of the sender
  ;; %:fromaddress : The email address of the sender
  ;; %:to, %:toname, %toaddress : Same for the recipient
  ;; %:date : Date of the message
  ;; %:date-timestamp : The date of the message as a active org timestamp
  ;;
  ;; Original Doom capture:
  ;; ("p" "Templates for projects")
  ;; ("pt" "Project-local todo" entry (file+headline +org-capture-project-todo-file "Inbox") "* TODO %?\n %i\n %a" :prepend t)
  ;; ("pn" "Project-local notes" entry (file+headline +org-capture-project-notes-file "Inbox") "* %U %?\n %i\n %a" :prepend t)
  ;; ("o" "Centralized templates for projects")
  ;; ("ot" "Project todo" entry #'+org-capture-central-project-todo-file "* TODO %?\n %i\n %a" :heading "Tasks" :prepend nil)
  ;; ("on" "Project notes" entry #'+org-capture-central-project-notes-file "* %U %?\n %i\n %a" :heading "Notes" :prepend t)

  ;; Code to automate task status, but i think some package exist that do a better job
  ;; (defun org-summary-todo (n-done n-not-done)
  ;;   "Switch entry to DONE when all subentries are done, to TODO otherwise."
  ;;   (let (org-log-done org-log-states)   ; turn off logging
  ;;     (org-todo (if (= n-not-done 0) "DONE" "TODO"))))
  ;; (add-hook 'org-after-todo-statistics-hook 'org-summary-todo)
  (setq gtd/started-head "🚀 Started:")
  (setq gtd/next-action-head "👉 Next actions:")
  (setq gtd/waiting-head  "⏳ Waiting on:")
  ;; (setq gtd/complete-head "Completed items:")
  (setq gtd/project-head "‼ Stuck projects:")
  (setq gtd/someday-head "💡 Someday/maybe:")
  (setq qaa/q "❓ All the Questions:")
  (setq qaa/a "ℹ All the Answers:")
  (setq review/done "🦾 Completed Tasks")
  (setq review/unfinished "🔀 Unfinished Scheduled Tasks")
  (setq org-agenda-start-day "+0")
  (setq org-agenda-custom-commands
        '(
          ("w" . "Work commands")
          ("p" . "Personal commands")
          ("wg" "GTD view"
           (
            (agenda "" ((org-agenda-span 'week)))
            (todo "STRT" ((org-agenda-overriding-header gtd/started-head)))
            (todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
            (todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
            ;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
            (stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
                       (org-agenda-overriding-header gtd/project-head)))
            (todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
            )
           ((org-agenda-tag-filter-preset '("+work")))
           )
          ("pg" "GTD view"
           (
            (agenda "" ((org-agenda-span 'week)))
            (todo "STRT" ((org-agenda-overriding-header gtd/started-head)))
            (todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
            (todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
            ;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
            (stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
                       (org-agenda-overriding-header gtd/project-head)))
            (todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
            )
           ((org-agenda-tag-filter-preset '("+perso")))
           )
          ("wq" "All the questions.."
           (
            (todo "[?]" ((org-agenda-overriding-header qaa/q)))
            (todo "[i]" ((org-agenda-overriding-header qaa/a)))
            )
           ((org-agenda-tag-filter-preset '("+work")))
           )
          ("pq" "All the questions.."
           (
            (todo "[?]" ((org-agenda-overriding-header qaa/q)))
            (todo "[i]" ((org-agenda-overriding-header qaa/a)))
            )
           ((org-agenda-tag-filter-preset '("+perso")))
           )
          ("wr" "Weekly Review"
           ((agenda ""
                    ((org-agenda-overriding-header review/done)
                     (org-agenda-skip-function '(org-agenda-skip-entry-if 'nottodo 'done))
                     (org-agenda-span 'week)
                     (org-agenda-start-day nil)))

            (agenda ""
                    ((org-agenda-overriding-header review/unfinished)
                     (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                     (org-agenda-span 'week)
                     (org-agenda-start-day nil)))
            )
           ((org-agenda-tag-filter-preset '("+work")))
           )
          ("pr" "Weekly Review"
           ((agenda ""
                    ((org-agenda-overriding-header review/done)
                     (org-agenda-skip-function '(org-agenda-skip-entry-if 'nottodo 'done))
                     (org-agenda-span 'week)
                     (org-agenda-start-day nil)))

            (agenda ""
                    ((org-agenda-overriding-header review/unfinished)
                     (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                     (org-agenda-span 'week)
                     (org-agenda-start-day nil)))
            )
           ((org-agenda-tag-filter-preset '("+perso")))
           )
          ("wp" "Planning"
           ((agenda ""
                    ((org-agenda-span 'month)
                     (org-agenda-start-day nil)
                     (org-agenda-overriding-header "📅 Sheduled Tasks"))
                    )

            (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-tag-filter-preset '("+work")))
           )
          ("pp" "Planning"
           ((agenda ""
                    ((org-agenda-span 'month)
                     (org-agenda-start-day nil)
                     (org-agenda-overriding-header "📅 Sheduled Tasks"))
                    )

            (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-tag-filter-preset '("+perso")))
           )
          ("wP" "Installation Bombardier"
           ((agenda ""
                    ((org-agenda-span 90)
                     (org-agenda-start-day "2026-01-29")
                     (org-agenda-overriding-header "📅 Installation Bombardier")
                     (org-agenda-prefix-format " %12t") ;; reserve time space
                     (org-agenda-todo-keyword-format "    %-5s") ;; fixed-width TODO
                     (org-agenda-tags-column -100) ;; right-align tags
                     (org-agenda-time-grid nil)
                     )))

           ((org-agenda-tag-filter-preset '("+BA_ON_SITE")))
          )
        )
  )
)
#+end_src

** Rust

#+begin_src emacs-lisp
(after! org
  ;; Rust code block setting
  (setq rustic-babel-display-error-popup nil)
)
#+end_src

** Latex

#+begin_src emacs-lisp
(after! org
  (setq

   ;; LaTeX math preview
   org-format-latex-options '(:foreground default :background default :scale 2 :html-foreground "Black"
                              :html-background "Transparent" :html-scale 1.0
                              :matchers ("begin" "$1" "$" "$$" "\\(" "\\["))
   )
)

(after! ox-latex
  (setq org-latex-compiler "xelatex"))
#+end_src

** Org Capture

#+begin_src emacs-lisp
(after! org
  (setq
   org-capture-templates
   '(
     ("t" "TODO" entry (file+olp +org-capture-todo-file "Tasks")
      "* IDEA %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i" :prepend t)
     ("c" "Code TODO" entry
      (file+olp +org-capture-todo-file "Tasks")
      "* TODO %a\n:PROPERTIES:\n:CREATED: %U\n:END:\n":prepend t)
     ("n" "Note" entry (file+olp +org-capture-notes-file "Notes")
      "* %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
     ("w" "Question" entry (file+olp +org-capture-notes-file "Questions")
      "* [?] %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
     ("j" "Journal" entry (file+olp +org-capture-journal-file "Journal")
      "* %?\n%a\n%i")
     ("m" "Meeting" entry (file+olp +org-capture-notes-file "Meetings")
      "* %? %U :meeting:\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n/Met with: /")
     ("a" "Appointment" entry (file+olp +org-capture-journal-file "Appointments")
      "* %? :appointment:\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i" :time-prompt t)
     ("e" "Email Workflow")
     ("ef" "Follow Up" entry (file+olp +org-capture-emails-file "Mails" "Follow Up") "* TODO Follow up with %:fromname on %a\nSCHEDULED:%t\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
     ("er" "Read Later" entry (file+olp +org-capture-emails-file "Mails" "Read Later") "* TODO Read: %a\nSCHEDULED:%t\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
     ("ew" "Waiting Response" entry
      (file+olp +org-capture-emails-file "Mails" "Waiting Response") "* WAIT Waiting response from %:toname on %a\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
     )
   )
  )
#+end_src
** Default apps
#+begin_src emacs-lisp
(after! org
  ;; Ensure exported HTML opens in the system browser, not inside Emacs
  (setq org-file-apps
        (append '(("\\.x?html?\\'" . default))
                (assq-delete-all "\\.x?html?\\'" org-file-apps))))
#+end_src
** +Org Superstar mode+
org-modern instead

#+begin_src emacs-lisp :tangle no
(add-hook 'org-mode-hook (lambda () (org-superstar-mode 1)))
#+end_src

* Markdown
#+begin_src emacs-lisp
(after! grip-mode
  (setq grip-binary-path "/home/tpouplier/.local/bin/grip")
  )
#+end_src

#+begin_src emacs-lisp
(after! markdown-mode
  (setq markdown-header-scaling t)
  (custom-set-faces
   '(markdown-header-face-1 ((t
                              :inherit markdown-header-face
                              :foreground "#fabd2f"
                              :height 1.8
                              :weight extra-bold)))
   '(markdown-header-face-2 ((t
                              :inherit markdown-header-face
                              :foreground "#fe8019"
                              :height 1.4
                              :weight extra-bold)))
   '(markdown-header-face-3 ((t
                              :inherit markdown-header-face
                              :foreground "#fb4934"
                              :height 1.2
                              :weight bold)))
   '(markdown-header-face-4 ((t
                              :inherit markdown-header-face
                              :foreground "#b8bb26"
                              :height 1.1
                              :weight bold)))
   '(markdown-header-face-5 ((t
                              :inherit markdown-header-face
                              :foreground "#83a598"
                              :height 1.0
                              :weight bold)))
   '(markdown-header-face-6 ((t
                              :inherit markdown-header-face
                              :foreground "#d3869b"
                              :height 1.0
                              :weight bold))))
  )
#+end_src

* PlantUML
#+begin_src emacs-lisp
;; Enable plantuml-mode for PlantUML files
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
#+end_src

** Exemple:

#+begin_src plantuml :tangle no
left to right direction
actor "Food Critic" as fc
rectangle Restaurant {
  usecase "Eat Food" as UC1
  usecase "Pay for Food" as UC2
  usecase "Drink" as UC3
}
fc --> UC1
fc --> UC2
fc --> UC3
#+end_src

#+RESULTS:
[[/tmp/babel-pg1Nry/plantuml-SHtP1g.png][/tmp/babel-pg1Nry/plantuml-SHtP1g.png]]

* Elgantt
Broken for now..
#+begin_src emacs-lisp :tangle no
(require 'cl-lib)
(require 'dash)
(require 'elgantt)
(unless (fboundp 'first) (defalias 'first #'car))

;; Clear rules to ensure the new global color logic takes effect immediately
(setq elgantt--display-rules nil)

(defface gortium/elgantt-weekend-face
  '((t (:background "#32302f" :extend nil))) 
  "Gruvbox Dark0_Hard/Soft mix for subtle weekend stripes.")

(defun gortium/internal--month-to-num (name)
  "Convert month string to number safely."
  (let ((case-fold-search t))
    (cond ((string-match-p "Jan" name) 1) ((string-match-p "Feb" name) 2)
          ((string-match-p "Mar" name) 3) ((string-match-p "Apr" name) 4)
          ((string-match-p "May" name) 5) ((string-match-p "Jun" name) 6)
          ((string-match-p "Jul" name) 7) ((string-match-p "Aug" name) 8)
          ((string-match-p "Sep" name) 9) ((string-match-p "Oct" name) 10)
          ((string-match-p "Nov" name) 11) ((string-match-p "Dec" name) 12) (t 1))))

(defun gortium/elgantt-draw-weekend-guides ()
  "Draw weekend guides for the ENTIRE buffer once to prevent scroll lag."
  (interactive)
  (when (derived-mode-p 'elgantt-mode)
    (let* ((inhibit-modification-hooks t)
           (header-line-1 (save-excursion 
                            (goto-char (point-min))
                            (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
           (col-indices '())
           (search-pos 0))

      (save-excursion
        (save-restriction
          (widen)
          ;; 1. Clear ALL weekend overlays in the entire buffer
          (remove-overlays (point-min) (point-max) 'gortium-weekend t)
          
          ;; 2. Parse header once to find column indexes (Fast)
          (while (string-match "|[[:space:]]*\\([[:alpha:]]+\\)[[:space:]]+\\([0-9]\\{4\\}\\)" header-line-1 search-pos)
            (let* ((month-start-col (match-beginning 0))
                   (month-name (match-string 1 header-line-1))
                   (year (string-to-number (match-string 2 header-line-1)))
                   (month-num (gortium/internal--month-to-num month-name))
                   (next-pipe (string-match "|" header-line-1 (1+ month-start-col)))
                   (month-width (if next-pipe (- next-pipe month-start-col 1) 31)))
              (dotimes (d month-width)
                (let* ((day (1+ d))
                       (time (condition-case nil (encode-time 0 0 12 day month-num year) (error nil))))
                  (when time
                    (let ((dow (nth 6 (decode-time time)))
                          (actual-col (+ month-start-col 1 d)))
                      (when (member dow '(0 6))
                        (push actual-col col-indices))))))
              (setq search-pos (or next-pipe (length header-line-1)))))

          ;; 3. Apply to the WHOLE buffer line by line
          (unless (null col-indices)
            (goto-char (point-min))
            (forward-line 2) ;; Skip headers
            (while (not (eobp))
              (let ((line-end (line-end-position)))
                (dolist (col col-indices)
                  (move-to-column col)
                  (let ((p (point)))
                    ;; Ensure we are still on the same line and at the correct column
                    (when (and (< p line-end) (= (current-column) col))
                      (let ((ov (make-overlay p (1+ p))))
                        (overlay-put ov 'face 'gortium/elgantt-weekend-face)
                        (overlay-put ov 'gortium-weekend t)
                        (overlay-put ov 'priority 100)
                        (overlay-put ov 'evaporate t))))))
              (forward-line 1)))))
      (message "Weekend guides rendered for the whole buffer."))))

;; Run it only once when the buffer is loaded
(add-hook 'elgantt-mode-hook #'gortium/elgantt-draw-weekend-guides)

(use-package! elgantt
  :commands (elgantt-open elgantt-open-current-org-file)
  :config
  ;; --- 1. Environment & UI ---
  (add-hook 'elgantt-mode-hook
            (lambda ()
              (setq-local org-phscroll-mode nil)
              (setq-local image-roll-mode nil)
              (setq truncate-lines t)))

  (setq elgantt-start-date "2026-01-01")

  (setq elgantt-header-column-offset 40
        elgantt-header-type 'root
        elgantt-show-header-depth t
        elgantt-insert-blank-line-between-top-level-header t
        elgantt-startup-folded nil
        elgantt-draw-overarching-headers nil
        elgantt-scroll-to-current-month-at-startup nil)

  (setq elgantt-user-set-color-priority-counter 0)

  ;; --- Rule 1: Active Timestamp Range ---
  (elgantt-create-display-rule draw-active-timestamp-range
    :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
                                 (split-string colors " "))))
             (range-dates . ((save-excursion
                               (org-back-to-heading t)
                               (let ((limit (save-excursion (outline-next-heading) (point))))
                                 (when (re-search-forward "<\\([^>]+\\)>--<\\([^>]+\\)>" limit t)
                                   (list (match-string 1) (match-string 2))))))))
    :args (elgantt-org-id)
    :body ((when (and elgantt-org-id range-dates)
             (let* ((colors (or override-color '("#fabd2f" "#fe8019")))
                    (s-str (substring (car range-dates) 0 10))
                    (e-str (substring (cadr range-dates) 0 10))
                    (p1 (save-excursion (when (elgantt--goto-date s-str) (point))))
                    (p2 (save-excursion (when (elgantt--goto-date e-str) (point)))))
               (when (and (numberp p1) (numberp p2))
                 (elgantt--draw-gradient
                  (car colors) (cadr colors)
                  (truncate p1) (truncate p2) nil
                  `(priority ,(setq elgantt-user-set-color-priority-counter
                                    (1- elgantt-user-set-color-priority-counter))
                    :elgantt-user-overlay ,elgantt-org-id)))))))

  ;; --- Rule 2: Effort Rule ---
  (elgantt-create-display-rule draw-scheduled-to-effort-end
    :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
                                 (split-string colors " "))))
             (elgantt-effort . ((org-entry-get (point) "EFFORT")))
             (wknd-days . ((when-let ((val (org-entry-get (point) "WEEKEND_DAYS")))
                             (string-to-number val)))))
    :args (elgantt-scheduled elgantt-effort elgantt-org-id)
    :body ((when (and elgantt-scheduled elgantt-effort)
             (let* ((start-ts (ts-parse elgantt-scheduled))
                    (raw-mins (org-duration-to-minutes elgantt-effort))
                    (total-days (+ (ceiling (/ (float raw-mins) 1440.0)) (or wknd-days 0)))
                    (p1 (save-excursion
                          (elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
                          (point)))
                    (colors (or override-color '("#8ec07c" "#458588"))))
               (when (numberp p1)
                 (if (<= total-days 1)
                     (elgantt--create-overlay (truncate p1) (1+ (truncate p1))
                                              `(face (:background ,(car colors))
                                                priority ,(setq elgantt-user-set-color-priority-counter
                                                                  (1- elgantt-user-set-color-priority-counter))
                                                :elgantt-user-overlay ,elgantt-org-id))
                   (let* ((end-ts (ts-adjust 'day (- total-days 2) start-ts))
                          (p2 (save-excursion
                                (elgantt--goto-date (ts-format "%Y-%m-%d" end-ts))
                                (point))))
                     (when (numberp p2)
                       (elgantt--draw-gradient
                        (car colors) (cadr colors)
                        (truncate p1) (1+ (truncate p2)) nil
                        `(priority ,(setq elgantt-user-set-color-priority-counter
                                          (1- elgantt-user-set-color-priority-counter))
                          :elgantt-user-overlay ,elgantt-org-id)))))))))))

  ;; --- Rule 3: Blocker Lines ---
  (elgantt-create-display-rule draw-blocker-lines
    :parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
    :args (elgantt-org-id elgantt-scheduled)
    :body ((when (and elgantt-org-id blocker-raw (not (string-empty-p blocker-raw)))
             (let* ((p-dest (save-excursion
                              (let ((d-start (or (when (stringp elgantt-scheduled) (substring elgantt-scheduled 0 10))
                                                 (elgantt-with-point-at-orig-entry nil
                                                   (save-excursion
                                                     (org-back-to-heading t)
                                                     (when (re-search-forward "<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" (line-end-position) t)
                                                       (match-string 1)))))))
                                (when (and d-start (elgantt--goto-date d-start)) (point))))))
               (when (numberp p-dest)
                 (let ((id-list (split-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw) "[ ,]+" t)))
                   (dolist (blocker-id id-list)
                     (save-excursion
                       (when (elgantt--goto-id blocker-id)
                         (let ((d-end-str nil)
                               (row-start (line-beginning-position))
                               (row-end (line-end-position)))
                           (elgantt-with-point-at-orig-entry nil
                             (save-excursion
                               (org-back-to-heading t)
                               (let ((limit (save-excursion (outline-next-heading) (point))))
                                 (if (re-search-forward "<[^>]+>--<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" limit t)
                                     (setq d-end-str (match-string 1))
                                   (let ((s (org-entry-get (point) "SCHEDULED"))
                                         (e (org-entry-get (point) "EFFORT"))
                                         (w (string-to-number (or (org-entry-get (point) "WEEKEND_DAYS") "0"))))
                                     (when (and s e)
                                       (setq d-end-str (ts-format "%Y-%m-%d" (ts-adjust 'day (1- (+ (ceiling (/ (float (org-duration-to-minutes e)) 1440.0)) w)) (ts-parse s))))))))))
                           (when d-end-str
                             (save-excursion
                               (elgantt--goto-date d-end-str)
                               (let ((p-source (point)))
                                 (if (and (>= p-source row-start) (<= p-source row-end))
                                     (elgantt--draw-line (truncate p-source) (truncate p-dest) "#b8bb26")
                                   (let ((col-offset (- p-source (save-excursion (goto-char p-source) (line-beginning-position)))))
                                     (goto-char row-start)
                                     (forward-char col-offset)
                                     (elgantt--draw-line (point) (truncate p-dest) "#b8bb26")))))))))))))))))

(defun elgantt-open-current-org-file ()
  (interactive)
  (if-let ((file (buffer-file-name)))
      (progn 
        (setq elgantt-agenda-files (list file)) 
        (elgantt--reset-org-ql-cache) 
        (elgantt-open))
    (message "No file!")))
#+end_src

#+RESULTS:
: elgantt-open-current-org-file

* Org Roam

** Static Path

#+begin_src emacs-lisp
(setq org-roam-directory (file-truename "~/ExoKortex/")
      org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db")
      org-attach-id-dir  "assets/"
      org-roam-dailies-directory "~/ExoKortex/2-Areas/Meta-Planning/Org/Journal/Daily")
#+end_src

** Custom config

#+begin_src emacs-lisp
(after! org-roam
  org-roam-completion-everywhere t
  org-roam-db-autosync-mode 1
  (require 'org-roam-dailies)

  (setq org-roam-dailies-capture-templates
        '(("n" "Note" entry "** %<%H:%M> 📝 %?"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :empty-lines 1)

          ("t" "Task" entry "** %<%H:%M> 🛠 %a"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :immediate-finish t
           :empty-lines 1)

          ("m" "Meeting" entry "** %<%H:%M> 👥 Meeting %?"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :empty-lines 1)

          ("u" "Muffin" entry "** %<%H:%M> 🥐 Muffin"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :immediate-finish t
           :empty-lines 1)

          ("e" "Event" entry "** %<%H:%M> 📅 Event: %?"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :empty-lines 1)

          ("c" "Clock In" entry "** %<%H:%M> ⏱ Clocked In"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :immediate-finish t
           :empty-lines 1)

          ("o" "Clock Out" entry "** %<%H:%M> 🚪 Clocked Out"
           :target (file+head+olp "%<%Y-%m-%d>.org"
                                  "#+title: %<%Y-%m-%d>\n\n* Journal\n"
                                  ("Journal"))
           :immediate-finish t
           :empty-lines 1)
        )
  )
)
#+end_src

** Custom function

*** New Entry

#+begin_src emacs-lisp
;; Org-roam-dailies new entry help function
;; Adapted from https://rostre.bearblog.dev/building-my-ideal-emacs-journal/
(defun gortium/org-roam-dailies-new-entry ()
  "Create a new entry in org-roam daily notes with interactive prompt.
Handles org-clock and context link capture for tasks."
  (interactive)
  (let* ((choices '(("Task 🛠" . task)
                    ("Note 📝" . note)
                    ("Meeting 👥" . meeting)
                    ("Muffin 🥐" . muffin)
                    ("Event 📅" . event)
                    ("Clock In ⏱" . in)
                    ("Clock Out 🚪" . out)))
         (choice-label (completing-read "Entry type: " (mapcar #'car choices)))
         (entry-type (cdr (assoc choice-label choices))))

    ;; Handle preconditions
    (cond
     ((eq entry-type 'task)
      (let ((check-and-clock-in
             (lambda ()
               (unless (org-entry-get nil "TODO" t)
                 (user-error "Point is not under a TODO heading"))
               (org-todo "STRT")
               (org-clock-in)
               (org-store-link nil t))))
        (cond
         ((eq major-mode 'org-agenda-mode)
          (save-window-excursion
            (org-agenda-switch-to)
            (funcall check-and-clock-in)))
         ((eq major-mode 'org-mode)
          (funcall check-and-clock-in))
         (t
          (user-error "Unsupported major mode for task entry")))))

     ((eq entry-type 'muffin)
      (org-clock-out))

     ((eq entry-type 'out)
      (org-clock-out))

     ((eq entry-type 'in)
      (org-clock-in)))

    ;; Launch capture
    (let ((key (pcase entry-type
                 ('note "n") ('task "t") ('meeting "m")
                 ('muffin "m") ('event "e")
                 ('in "c") ('out "o"))))
      (org-roam-dailies-capture-date nil nil key))))
#+end_src

*** Connecteam

#+begin_src emacs-lisp
;; Connecteam org-roam-dailies integration
(require 'request)

(defvar gortium/connecteam-api-key (auth-source-passage-get 'secret "connecteam") "Your Connecteam API Key")
(defvar gortium/connecteam-user-id "9885891" "Your Connecteam User ID")
(defvar gortium/connecteam-clock-id "9335145" "Your Connecteam time clock ID used in API calls.")

(defun gortium/connecteam-clock-in (job-id time-clock-id)
  "Send a clock-in request to Connecteam API for JOB-ID and TIME-CLOCK-ID."
  (request
    (format "https://api.connecteam.com/time-clock/v1/time-clocks/%s/clock-in" time-clock-id)
    :type "POST"
    :headers `(("X-API-KEY" . ,gortium/connecteam-api-key)
               ("accept" . "application/json")
               ("content-type" . "application/json"))
    :data (json-encode `((userId . ,gortium/connecteam-user-id)
                         (jobId . ,job-id)))
    :parser 'json-read
    :success (cl-function
              (lambda (&key data &allow-other-keys)
                (message "Connecteam clock-in successful: %s" (alist-get 'message data))))
    :error (cl-function
            (lambda (&rest args &key error-thrown &allow-other-keys)
              (message "Connecteam clock-in failed: %S" error-thrown)))))

(defun gortium/connecteam-clock-out (time-clock-id)
  "Send a clock-out request to Connecteam API for TIME-CLOCK-ID."
  (request
    (format "https://api.connecteam.com/time-clock/v1/time-clocks/%s/clock-out" time-clock-id)
    :type "POST"
    :headers `(("X-API-KEY" . ,gortium/connecteam-api-key)
               ("accept" . "application/json")
               ("content-type" . "application/json"))
    :data (json-encode `((userId . ,gortium/connecteam-user-id)))
    :parser 'json-read
    :success (cl-function
              (lambda (&key data &allow-other-keys)
                (message "Connecteam clock-out successful: %s" (alist-get 'message data))))
    :error (cl-function
            (lambda (&rest args &key error-thrown &allow-other-keys)
              (message "Connecteam clock-out failed: %S" error-thrown)))))

(defun gortium/org-clock-in-to-connecteam ()
  "Clock in to Connecteam using the job ID in the current Org task."
  (when (org-entry-get nil "CONNECTEAM_JOB_ID" t)
    (let ((job-id (org-entry-get nil "CONNECTEAM_JOB_ID" t)))
      (gortium/connecteam-clock-in job-id gortium/connecteam-clock-id))))

(add-hook 'org-clock-in-hook #'gortium/org-clock-in-to-connecteam)

(defun gortium/org-clock-out-from-connecteam ()
  "Clock out from Connecteam or trigger vacation request if MUFFIN property is set."
  (let ((muffin (org-entry-get nil "MUFFIN")))
    (if muffin
        (gortium/connecteam-vacation-request)
      (gortium/connecteam-clock-out gortium/connecteam-clock-id))))

(add-hook 'org-clock-out-hook #'gortium/org-clock-out-from-connecteam)

(after! calendar
  ;; Define a function to open org-roam daily note for selected date in calendar
  (defun gortium/org-roam-dailies-open-at-point ()
    "Open org-roam daily note for date at point in calendar."
    (interactive)
    (let ((date (calendar-cursor-to-date)))
      (org-roam-dailies-goto-date date)))
  )

;; Bind RET in calendar mode to open daily note instead of default exit
(evil-define-key 'normal calendar-mode-map
  (kbd "RET") #'gortium/org-roam-dailies-open-at-point)
#+end_src

*** Old config

#+begin_src emacs-lisp :tangle no
#+end_;; (use-package org-journal
;;   :defer t
;;   :custom
;;   (org-journal-dir "~/notes/journal/")
;;   (org-journal-enable-agenda-integration t))
#+end_src

* IDE

** Dap Mode

#+begin_src emacs-lisp
(after! dap-mode
  (require 'dap-python)
  (setq dap-python-debugger 'debugpy)
  (dap-ui-mode 1)
  (add-hook 'dap-terminated-hook
            (lambda ()
              (dap-ui-controls-mode -1)
              (dap-ui-mode -1))))

(after! dap-python
  (dap-register-debug-template
   "Python :: My Script with Args"
   (list :type "python"
         :args ["--start" "73"
                "--input" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/Resources/G05730212_111 scan plans/curve_G05730212_111.src"
                "--template" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/exit_strat/ExitPath_Template.src"
                "--output" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/exit_strat/output/ExitPath.src"]
         :cwd nil
         :module nil
         :program nil
         :request "launch"
         :name "Python :: My Script with Args")))
#+end_src

** +Dape+
Tried it, was not able to make it work, so back to dap mode for now

#+begin_src emacs-lisp :tangle no
(use-package dape
  :preface
  ;; By default dape shares the same keybinding prefix as `gud'
  ;; If you do not want to use any prefix, set it to nil.
  ;; (setq dape-key-prefix "\C-x\C-a")

  :hook
  ;; Save breakpoints on quit
  (kill-emacs . dape-breakpoint-save)
  ;; Load breakpoints on startup
  (after-init . dape-breakpoint-load)

  :custom
  ;; Turn on global bindings for setting breakpoints with mouse
  (dape-breakpoint-global-mode +1)

  ;; Info buffers to the right
  ;; (dape-buffer-window-arrangement 'right)
  ;; Info buffers like gud (gdb-mi)
  ;; (dape-buffer-window-arrangement 'gud)
  ;; (dape-info-hide-mode-line nil)

  ;; Projectile users
  (dape-cwd-function 'projectile-project-root)

  :config
  ;; Pulse source line (performance hit)
  (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line)

  ;; Save buffers on startup, useful for interpreted languages
  (add-hook 'dape-start-hook (lambda () (save-some-buffers t t)))

  ;; Kill compile buffer on build success
  ;; (add-hook 'dape-compile-hook 'kill-buffer)
  )

;; For a more ergonomic Emacs and `dape' experience
(use-package repeat
  :custom
  (repeat-mode +1))
#+end_src

** LSP
I had to work on C# Framework 4.8..

#+begin_src emacs-lisp
;; LSP BABY
(after! lsp-mode
  (setq lsp-csharp-server-path "~/.local/tools/omnisharp-mono/omnisharp.sh")
  )
#+end_src

** Scroll-Margin
To keep my eye in the center of the screen while scrolling. Like in my nvim x)
Deactivated for ultra-scroll

#+begin_src emacs-lisp
;; (setq scroll-margin 10)
#+end_src

** Visual Line Mode

#+begin_src emacs-lisp
;; Enable visual line wrapping globally
;; (global-visual-line-mode 1)
;; Enable visual line wrapping in certain modes
(after! org
  (add-hook 'org-mode-hook #'visual-line-mode))
(after! prog-mode
  (add-hook 'prog-mode-hook #'visual-line-mode))
(after! text-mode
  (add-hook 'text-mode-hook #'visual-line-mode))
#+end_src

** Org-phscroll
Load org-phscroll after org. This allow horizontal scroll in tables. (No line wrap in table)

#+begin_src emacs-lisp
(use-package! org-phscroll
  :after org)
#+end_src

** Spellchecking

*** Ispell
Now I can write x) (spellchecking)

#+begin_src emacs-lisp
(after! ispell
  (setq ispell-program-name "hunspell"
        ispell-dictionary "en_CA,fr_CA"
        ispell-personal-dictionary "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/perso.dic")

  ;; Set the dictionary path environment variable
  (setenv "DICPATH" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries")

  (setq ispell-hunspell-dict-paths-alist
        '(("fr_CA" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/fr_CA.aff")
          ("en_CA" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/en_CA.aff")))

  (setq ispell-hunspell-dictionary-alist
        '(("fr_CA" "[[:alpha:]]" "[^[:alpha:]]" "[']" nil ("-d" "fr_CA") nil utf-8)
          ("en_CA" "[[:alpha:]]" "[^[:alpha:]]" "[']" nil ("-d" "en_CA") nil utf-8)))

  ;; ispell-set-spellchecker-params has to be called
  ;; before ispell-hunspell-add-multi-dic will work
  (ispell-set-spellchecker-params)
  (ispell-hunspell-add-multi-dic "en_CA,fr_CA"))
#+end_src

*** Writegood Mode
Let me write like a broken engineer, thank you.

#+begin_src emacs-lisp
(add-hook 'writegood-mode-hook 'writegood-passive-voice-turn-off)
#+end_src

** Drag-Stuff

#+begin_src emacs-lisp
;; Move selected region up or down
(use-package! drag-stuff
  :config
  ;; Enable drag-stuff-mode globally
  (drag-stuff-global-mode 1)

  ;; Set keybindings
  (map! :v "M-j" #'drag-stuff-down
        :v "M-k" #'drag-stuff-up
        :n "M-j" #'drag-stuff-down
        :n "M-k" #'drag-stuff-up)

  ;; Fold bindings
  (after! evil
    (define-key evil-motion-state-map (kbd "z C") #'hs-hide-level))

  ;; Disable global bindings for C-j and C-k
  (global-unset-key (kbd "C-j"))
  (global-unset-key (kbd "C-k"))

  ;; Optionally define default keybindings (if needed)
  (drag-stuff-define-keys))
#+end_src

** Line Number Type
This determines the style of line numbers in effect. If set to ~nil~, line
numbers are disabled. For relative line numbers, set this to ~relative~.

#+begin_src emacs-lisp
(setq display-line-numbers-type 'relative)
#+end_src

** Evile-Escape (JK)
JK to escape was not working. Added it back.

#+begin_src emacs-lisp
(use-package! evil-escape
  :config
  (setq evil-escape-excluded-states '(normal visual multiedit emacs motion)
        evil-escape-excluded-major-modes '(neotree-mode treemacs-mode vterm-mode)
        evil-escape-key-sequence "jk"
        evil-escape-delay 0.15)
  (evil-escape-mode 1)
  )
#+end_src

** Nix-Mode

#+begin_src emacs-lisp
(after! nix-mode
  (setq lsp-nix-nil-formatter ["nixpkgs-fmt"])
  (add-hook 'nix-mode-hook #'lsp!))

(after! lsp-mode
  (setq lsp-nix-server 'nil))
#+end_src

** KRL Mode
Im not working in WorkVisual. Thank you. (Kuka Robot stuff)

#+begin_src emacs-lisp
(add-hook 'krl-mode-hook 'font-lock-mode)
(add-hook 'krl-mode-hook 'display-line-numbers-mode)
(use-package! krl-mode
  :mode (("\\.src\\'" . krl-mode)
         ("\\.dat\\'" . krl-mode)
         ("\\.sub\\'" . krl-mode)))

(use-package! auto-highlight-symbol
  :hook (krl-mode . auto-highlight-symbol-mode)
  :config
  (setq ahs-idle-interval 0.5          ; highlight after 0.5s
        ahs-default-range 'ahs-range-whole-buffer  ; highlight in whole buffer
        ahs-case-fold-search t          ; case-INsensitive matching
        ahs-include-definition t))      ; highlight definition too
#+end_src

** KRL formater
I'm a formatting nazi now xD

#+begin_src emacs-lisp
(defcustom krl-formatter-script-path "~/ExoKortex/1-Projects/Exit_strat/robot_program_formater/scripts/krl_formatter.py"
  "Path to the KRL formatter script."
  :type 'string
  :group 'krl)

(defun krl-format-buffer ()
  "Format the current buffer using the KRL formatter."
  (interactive)
  (let ((original-point (point))
        (original-line (line-number-at-pos)))
    (shell-command-on-region
     (point-min)
     (point-max)
     (concat "python " krl-formatter-script-path)
     t t)
    ;; Try to restore cursor position
    (goto-char (point-min))
    (forward-line (1- original-line))
    (message "KRL buffer formatted")))

(defun krl-format-region (start end)
  "Format the selected region using the KRL formatter."
  (interactive "r")
  (shell-command-on-region
   start
   end
   (concat "python " krl-formatter-script-path)
   t t)
  (message "KRL region formatted"))

;; Auto-format on save (optional)
(defun krl-format-before-save ()
  "Format KRL code before saving."
  (when (eq major-mode 'krl-mode)
    (krl-format-buffer)))

;; Uncomment the next line to enable auto-formatting on save
;; (add-hook 'before-save-hook 'krl-format-before-save)
#+end_src

** Hledger Mode

#+begin_src emacs-lisp
(use-package! hledger-mode
  :config
  (setq hledger-jfile "~/2-Areas/Finances/finance_vault/tpouplier.hledger")
  (setq hledger-top-asset-account "Assets")
  (setq hledger-top-income-account "Incomes")
  (setq hledger-top-expense-account "Expenses")
  (setq hledger-ratios-debt-accounts "Liabilities")
  (setq hledger-ratios-assets-accounts "Assets")
  (setq hledger-ratios-income-accounts "Incomes")
  (setq hledger-ratios-liquid-asset-accounts "Assets:Cash")
  (setq hledger-ratios--accounts "Assets:Cash")
  ;; (setq hledger-ratios-debt-accounts "Liabilities")
  (add-to-list 'auto-mode-alist '("\\.hledger\\'" . hledger-mode))
  )
#+end_src

** Fragtog

#+begin_src emacs-lisp
;; Hook fragtog to org-mode to have editable latex preview
(use-package! org-fragtog
  :hook (org-mode . org-fragtog-mode))

;; Rescale latex preview when changing font size
(defun gortium/org-latex-refresh-on-zoom (&rest _)
  "Dynamically rescale and refresh all LaTeX previews after text zoom."
  (when (derived-mode-p 'org-mode)
    ;; Dynamically update scale relative to zoom level
    (setq org-format-latex-options
          (plist-put org-format-latex-options
                     :scale (+ 2 (* 0.5 text-scale-mode-amount))
                     ))
    ;; Clear all previews
    (org-clear-latex-preview)
    ;; Re-render all previews
    (org-latex-preview '(16))
    ))

(advice-add 'text-scale-increase :after #'gortium/org-latex-refresh-on-zoom)
(advice-add 'text-scale-decrease :after #'gortium/org-latex-refresh-on-zoom)
(advice-add 'text-scale-set :after #'gortium/org-latex-refresh-on-zoom)
#+end_src

** Age
Allow me to edit encryted age file directly in emacs buffer.

#+begin_src emacs-lisp
(use-package! age
  :demand t
  :config
  (setq age-program "rage")
  (setq age-default-identity "~/.ssh/gortium_ssh_key")
  (setq age-default-recipient "~/.ssh/gortium_ssh_key.pub")
  (age-file-enable))
#+end_src

#+begin_src emacs-lisp
(require 'notifications)
(require 'cl-lib)

(defun gortium/age-notify (msg &optional simple)
  "Notify about AGE operations. SIMPLE uses `message` instead of desktop notification."
  (if simple
      (message "%s" msg)
    (if (eq system-type 'gnu/linux)
        (notifications-notify
         :title "age.el"
         :body msg
         :urgency 'low
         :timeout 800)
      (message "%s" msg))))

(defun gortium/age-notify-decrypt (&rest args)
  "Notification hook for age decryption."
  (cl-destructuring-bind (context cipher) args
    (gortium/age-notify (format "Decrypting %s" (age-data-file cipher)) t)))

(defun gortium/age-notify-encrypt (&rest args)
  "Notification hook for age encryption."
  (cl-destructuring-bind (context plain recipients) args
    (gortium/age-notify (format "Encrypting %s" (age-data-file plain)) t)))

(defun gortium/age-toggle-decrypt-notifications ()
  "Toggle notifications for age decryption."
  (interactive)
  (if (advice-member-p #'gortium/age-notify-decrypt #'age-start-decrypt)
      (progn
        (advice-remove #'age-start-decrypt #'gortium/age-notify-decrypt)
        (message "Disabled age decrypt notifications."))
    (advice-add #'age-start-decrypt :before #'gortium/age-notify-decrypt)
    (message "Enabled age decrypt notifications.")))

(defun gortium/age-toggle-encrypt-notifications ()
  "Toggle notifications for age encryption."
  (interactive)
  (if (advice-member-p #'gortium/age-notify-encrypt #'age-start-encrypt)
      (progn
        (advice-remove #'age-start-encrypt #'gortium/age-notify-encrypt)
        (message "Disabled age encrypt notifications."))
    (advice-add #'age-start-encrypt :before #'gortium/age-notify-encrypt)
    (message "Enabled age encrypt notifications.")))

;; enable notifications by default
(gortium/age-toggle-decrypt-notifications)
(gortium/age-toggle-encrypt-notifications)
#+end_src

** Passage
Allow retrieval of password from age file formatted like passwordstore

#+begin_src emacs-lisp
(use-package! passage
  :demand t
  :config
  ;; rebind function value for pass to passage
  (fset #'pass (lambda () (interactive) (passage)))
  (setq age-program "rage")
  (setq auth-source-passage-filename (expand-file-name "~/ExoKortex/2-Areas/IT/dotfiles/secrets"))
  (setenv "PASSAGE_IDENTITIES_FILE" (expand-file-name age-default-identity))
  (setenv "PASSAGE_RECIPIENTS_FILE" (expand-file-name age-default-recipient))
  (setenv "PASSAGE_AGE" "rage")
  (setenv "PASSAGE_DIR" (expand-file-name "~/ExoKortex/2-Areas/IT/dotfiles/secrets"))
  )
#+end_src
** Diff HL Mode
Show git changes in the sidebar

#+begin_src emacs-lisp
(diff-hl-mode +1)
#+end_src

* Terminal

** +EEE+

#+begin_src emacs-lisp :tangle no
;; TUI tools in emacs
(after! eee
  (setq ee-terminal-command "kitty")
  )
#+end_src

** Vterm

#+begin_src emacs-lisp
;; Ensure C-j works properly in insert state in vterm
(after! vterm
  (add-hook 'vterm-mode-hook
            (lambda ()
              (evil-local-set-key 'insert (kbd "C-j") #'vterm--self-insert))))
#+end_src

* AI
GPTel and opencode are the winner for now.

** GPTel
The best LLM integration I found. Works with MCP server for more functionnalities. And Rewrite is awesome.

#+begin_src emacs-lisp
;; GPTel AI chat for emacs
(use-package! gptel
  :config
  ;; (advice-add 'gptel-rewrite :after #'gptel-rewrite-diff)
  (add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
  (setq gptel-display-buffer-action
        '(display-buffer-same-window))
  (defun gptel-set-default-directory ()
    (unless (buffer-file-name)
      (setq default-directory "~/ExoKortex/3-Resources/AI-Chat/")))
  (add-hook 'gptel-mode-hook #'gptel-set-default-directory)

  (setq gptel-expert-commands t
        gptel-default-mode 'org-mode
        ;; gptel-model 'OpenRouter:deepseek/deepseek-chat-v3-0324:free
        gptel-api-key (auth-source-passage-get 'secret "openrouter"))

  (require 'gptel-integrations)
  (gptel-make-openai "OpenRouter"
    :host "openrouter.ai"
    :endpoint "/api/v1/chat/completions"
    :stream t
    :key (auth-source-passage-get 'secret "openrouter")
    :models '(deepseek/deepseek-r1-0528-qwen3-8b:free
              google/gemini-2.0-flash-exp:free
              deepseek/deepseek-chat-v3-0324:free
              meta-llama/llama-4-maverick:free
              mistralai/devstral-2512:free
              qwen/qwen3-coder:free))
  (gptel-make-gemini "Gemini"
    :key (auth-source-passage-get 'secret "gemini")
    :stream t
    :models '(gemini-3-pro-preview
              gemini-2.5-pro
              gemini-3-pro
              gemini-2.5-flash))
  (gptel-make-ollama "Ollama"             
    :host "localhost:11434"               
    :stream t                             
    :models '(deepseek-r1:1.5b
              gemini-3-pro-preview
              orieg/gemma3-tools:4b))            ;List of models
  (gptel-make-openai "OpenWebUI"
    :host "ai.aziworkhorse.duckdns.org"
    :curl-args '("--insecure") ; needed for self-signed certs
    :key (auth-source-passage-get 'secret "openwebui")
    :endpoint "/api/chat/completions"
    :stream t
    :models '("orieg/gemma3-tools:1b"))
  (gptel-make-openai "MistralLeChat"
    :host "api.mistral.ai"
    :endpoint "/v1/chat/completions"
    :protocol "https"
    :key (auth-source-passage-get 'secret "mistral")
    :models '("devstral-2512"))
  (setq-default gptel-backend (gptel-get-backend "MistralLeChat")
                gptel-model 'devstral-2512))
#+end_src

*** Rewrite
Really useful to do stuff you dont want to do
#+begin_src emacs-lisp
(map! :after gptel
      :leader
      (
       :prefix ("r" . "GPTel Rewrite")
       :desc "Rewrite region"     "r" #'gptel-rewrite
       :desc "Show rewrite diff"  "d" #'gptel--rewrite-diff
       :desc "Accept rewrite"     "a" #'gptel--rewrite-accept
       :desc "Reject rewrite"     "x" #'gptel--rewrite-reject
       ))
#+end_src

** Magit-gptel
#+begin_src emacs-lisp
(defun gortium/magit-gptel-generate-commit ()
  "Generate a professional Git commit message from staged changes."
  (interactive)

  (unless (and buffer-file-name
               (string-match-p "COMMIT_EDITMSG" buffer-file-name))
    (user-error "Not in a commit message buffer"))

  (let ((diff (magit-git-string "diff" "--cached"))
        (buf (current-buffer)))
    (when (or (null diff) (string-empty-p diff))
      (user-error "No staged changes"))

    (gptel-request
     (format
      "You are a professional software engineer writing a Git commit message.

IMPORTANT RULES:
- You MUST only use information contained in the diff below.
- Do NOT guess or invent changes.
- If the diff is unclear, write a generic message such as:
  'Update files with staged changes' and do NOT mention specific file details.
- The first line must be imperative and <= 50 characters.
- Provide a short bullet list in the body (2–4 bullets max).

DIFF:
%s"
      diff)
     :stream nil
     :callback
     (lambda (response _info)
       (with-current-buffer buf
         (erase-buffer)
         (insert (string-trim response) "\n"))))))

(map! :after magit
      :map git-commit-mode-map
      :localleader
      :desc "GPT generate commit"
      "g" #'gortium/magit-gptel-generate-commit)

#+end_src

** +MCP+
May reuse later with opencode or gptel, but for now, deactivated

#+begin_src emacs-lisp :tangle no
#+end_(use-package! mcp
  :ensure t
  :after gptel
  ;; :custom (mcp-hub-servers
  ;;          `(
  ;;            ("serena" . (
  ;;                         :command "uvx"
  ;;                         :args ("--from", "git+https://github.com/oraios/serena", "serena-mcp-server")))
  ;;            ("codemcp" . (:command "uvx"
  ;;                          :args ("--from", "git+https://github.com/ezyang/codemcp@prod", "codemcp")))
  ;;            ))
  :config (require 'mcp-hub)
  :hook (after-init . mcp-hub-start-all-server))
#+end_src

** +Emigo+

#+begin_src emacs-lisp :tangle no
Emigo - Aidermacs 2.0
(use-package! emigo
  :config
  (emigo-enable) ;; Starts the background process automatically
  :custom
  ;; Encourage using OpenRouter with Deepseek
  (emigo-model "openrouter/deepseek/deepseek-chat-v3-0324:free")
  (emigo-base-url "https://openrouter.ai/api/v1")
  (emigo-api-key (auth-source-passage-get 'secret "openrouter")))

;; Enable line wrapping in Emigo chat buffers
(add-hook 'emigo-mode-hook
          (lambda ()
            (setq-local truncate-lines nil)
            (visual-line-mode 1)))

;; Python venv for emacs (just emigo req for now)
(use-package pyvenv
  :after python
  :config
  (pyvenv-activate "~/.venvs/emacs"))
#+end_src

** +Aidermacs+

#+begin_src emacs-lisp :tangle no
;; Aidermacs - aider for emacs - peer progrmming
(use-package! aidermacs
  :bind (("C-c a" . aidermacs-transient-menu))
  :config
  ;; Set API_KEY in .bashrc, that will automatically picked up by aider or in elisp
  ;; (setenv "ANTHROPIC_API_KEY" "sk-...")
  ;; defun my-get-openrouter-api-key yourself elsewhere for security reasons
  (setenv "OPENROUTER_API_KEY" (auth-source-passage-get 'secret "openrouter"))
  :custom
  ;; See the Configuration section below
  (aidermacs-use-architect-mode t)
  ;; Default model used for all modes unless overridden
  (aidermacs-default-model "deepseek/deepseek-chat-v3-0324:free")
  ;; Optional: Set specific model for architect reasoning
  (aidermacs-architect-model "deepseek/deepseek-r1-0528-qwen3-8b:free")
  ;; Optional: Set specific model for code generation
  (aidermacs-editor-model "deepseek/deepseek-chat-v3-0324:free")
  ;; default to nil
  (aidermacs-weak-model "deepseek/deepseek-chat-v3-0324:free")
  ;; Use vterm backend (default is comint)
  (aidermacs-backend 'vterm)
  ;; Multiline keybind
  (aidermacs-vterm-multiline-newline-key "S-<return>"))
#+end_src

** +Ellama+

#+begin_src emacs-lisp :tangle no
(use-package ellama
  :ensure t
  :bind ("C-c e" . ellama)
  ;; send last message in chat buffer with C-c C-c
  :hook (org-ctrl-c-ctrl-c-final . ellama-chat-send-last-message)
  :init
  (setq llm-warn-on-nonfree nil)
  (setopt ellama-auto-scroll t)
  (require 'llm-ollama)
  (setopt ellama-provider
          (make-llm-ollama
           ;; this model should be pulled to use it
           ;; value should be the same as you print in terminal during pull
           :chat-model "deepseek-r1:1.5b"
           :embedding-model "nomic-embed-text:v1.5"))
  (require 'llm-openai)
  (setopt ellama-providers
          '(
            ("deepseek-r1" . (make-llm-openai-compatible
                              :key (auth-source-passage-get 'secret "openrouter")
                              :url "https://openrouter.ai/api/v1"
                              :chat-model "deepseek/deepseek-chat-v3-0324:free"))
            ("gemini-2.0-flash" . (make-llm-openai-compatible
                                   :key (auth-source-passage-get 'secret "openrouter")
                                   :url "https://openrouter.ai/api/v1"
                                   :chat-model "google/gemini-2.0-flash-exp:free"))
            )
          )
  :config
  ;; show ellama context in header line in all buffers
  (ellama-context-header-line-global-mode +1)
  ;; show ellama session id in header line in all buffers
  (ellama-session-header-line-global-mode +1))
#+end_src

** +Elisa+

#+begin_src emacs-lisp :tangle no
(use-package elisa
  :init
  (setopt elisa-limit 5)
  ;; reranker increases answer quality significantly
  (setopt elisa-reranker-enabled nil)
  ;; prompt rewriting may increase quality of answers
  ;; disable it if you want direct control over prompt
  (setopt elisa-prompt-rewriting-enabled t)
  (require 'llm-ollama)
  ;; gemma 2 works very good in my use cases
  ;; it also boasts strong multilingual capabilities
  ;; (setopt elisa-chat-provider
  ;; 	  (make-llm-ollama
  ;; 	   :chat-model "gemma2:9b-instruct-q6_K"
  ;; 	   :embedding-model "snowflake-arctic-embed2"
  ;; 	   ;; set context window to 8k
  ;; 	   :default-chat-non-standard-params '(("num_ctx" . 8192))))
  ;;
  ;; qwen 2.5 3b works good in my test cases and provide longer context
  ;; (setopt elisa-chat-provider
  ;;         (make-llm-ollama
  ;;          :chat-model "qwen2.5:3b"
  ;;          :embedding-model "snowflake-arctic-embed2"
  ;;          :default-chat-temperature 0.1
  ;;          :default-chat-non-standard-params '(("num_ctx" . 32768))))
  (require 'llm-ollama)
  (setopt elisa-chat-provider
          (make-llm-ollama
           ;; this model should be pulled to use it
           ;; value should be the same as you print in terminal during pull
           :chat-model "deepseek-r1:1.5b"
           :embedding-model "nomic-embed-text:v1.5"))
  (require 'llm-openai)
  (setopt elisa-chat-provider
          (make-llm-openai-compatible
           :key (auth-source-passage-get 'secret "openrouter")
           :url "https://openrouter.ai/api/v1"
           :chat-model "deepseek/deepseek-chat-v3-0324:free")
          llm-warn-on-nonfree nil)
  (setopt elisa-embeddings-provider (make-llm-ollama :embedding-model "nomic-embed-text:v1.5"))
  ;; this embedding model has stong multilingual capabilities
  ;; (setopt elisa-embeddings-provider (make-llm-ollama :embedding-model "snowflake-arctic-embed2"))
  ;; enable batch embeddings for faster processing
  (setopt elisa-batch-embeddings-enabled t)
  :config
  ;; searxng works better than duckduckgo in my tests
  (setopt elisa-web-search-function 'elisa-search-searxng))
#+end_src

** +Org-AI+

#+begin_src emacs-lisp :tangle no
Org-AI
(setq org-ai-openai-api-token "")

(use-package! org-ai
  :ensure t
  :commands (org-ai-mode
             org-ai-global-mode)
  :init
  (add-hook 'org-mode-hook #'org-ai-mode) ; enable org-ai in org-mode
  (org-ai-global-mode) ; installs global keybindings on C-c M-a
  :config
  (setq org-ai-auto-fill t) ;; for automatic word wrap in fill-column length
  (setq org-ai-jump-to-end-of-block nil) ;; to make it not auto-jump to the end of the respose
  ;; Select your ai model
  ;; (setq org-ai-default-chat-model "gpt-3.5-turbo")
  (setq org-ai-default-chat-model "gpt-4o-mini")
  ;; (setq org-ai-default-chat-model "gpt-4")
  (org-ai-install-yasnippets)) ; if you are using yasnippet and want `ai` snippets
#+end_src

* Tramp
Remote connection to server inside emacs (with all my config) <3

#+begin_src emacs-lisp
;; set `tramp-direct-async-process' locally in all ssh connections
(connection-local-set-profile-variables
 'remote-direct-async-process
 '((tramp-direct-async-process . t)))
(connection-local-set-profiles
 '(:application tramp :protocol "ssh")
 'remote-direct-async-process)

(after! tramp
  (setq tramp-verbose 10)
  ;; Custom TRAMP configuration for Windows SSH ;; (add-to-list 'tramp-connection-properties
  ;;              (list "ssh"
  ;;                    "Ingenuity-win"
  ;;                    "tramp-shell-prompt-pattern"
  ;;                    "\\$ $"))
  )

#+end_src

* Dirvish
Allow fast navigation and preview to files.
Recently added the quick access entries. Game changer.

#+begin_src emacs-lisp
;; Dirvish config
(after! (dirvish dired)
  (require 'async)
  (dired-async-mode 1)
  ;; Display icons, file size, timestamps, etc.
  (setq dirvish-attributes
        '(nerd-icons
          subtree-state
          file-size
          file-time))

  (setq dirvish-default-layout '(0.3 0.15 0.55))

  ;; Use a header line instead of the traditional dired modeline
  (setq dirvish-use-header-line t)

  ;; Show dotfile please, ty
  ;; (setq dired-omit-mode nil)
  ;; (setq dired-listing-switches "-a")

  ;; Replace default dired mode globally
  (dirvish-override-dired-mode)

  (require 'dirvish-yank)

  (setq! dirvish-quick-access-entries
         `(
           ("h" "~/"                          "Home")
           ("k" "~/ExoKortex/2-Areas/Meta-Planning/Org/Core"                "ExoKortex")
           ("p" "~/ExoKortex/1-Projects"      "Projects")
           ("a" "~/ExoKortex/2-Areas"         "Areas")
           ("r" "~/ExoKortex/3-Resources"     "Resources")
           ("i" "~/ExoKortex/4-Archives"      "Archives")
           ("d" "~/Downloads/"                "Downloads")
           ("u" "/run/media"        "Mounted drives")
           ("t" "~/.local/share/Trash/files/" "Trash")
           ))
  (setq dirvish-yank-use-rsync t)
  (setq dirvish-yank-rsync-args '("-ah" "--info=progress2"))
)

;; Use `p` to open the yank menu
(map! :after dirvish-yank
      :map dirvish-mode-map
      :n "p" #'dirvish-yank-menu)
#+end_src

#+RESULTS:

* Consult-ripgrep

#+begin_src emacs-lisp
;; Enable previewing of surrounding lines in consult-ripgrep
(setq consult-ripgrep-preview t)
#+end_src

* Weather

#+begin_src emacs-lisp
;; Weather
(after! wttrin
  (setq wttrin-default-cities '("Blainville" "Canada"))
  )
;; Not working..
;; (setq weather-metno-location-name "Blainville, Canada"
;;       weather-metno-location-latitude 45
;;       weather-metno-location-longitude 73)
#+end_src

* Org-Modern
Make org task, agenda, tables look nicer, but you need to deactivate it sometimes to really see what is going on.

#+begin_src emacs-lisp
;; Modern look for org
(use-package! org-modern
  :after org
  :config
  (global-org-modern-mode)
  (setq org-modern-star 'replace)
  (setq org-modern-todo-faces
        '(
          ;; Framework
          ("EPIC" :foreground "#b16286" :inverse-video t :weight bold)   ; large project
          ("AREA" :foreground "#83a598" :inverse-video t :weight bold)   ; domain
          ("PROJ" :foreground "#458588" :inverse-video t :weight bold)   ; specific project

          ;; Tasks
          ("TODO" :foreground "#fb4934")                                 ; to do
          ("STRT" :foreground "#fe8019" :weight bold :inverse-video t)   ; started
          ("NEXT" :foreground "#fb4934" :weight bold :underline t)       ; next action
          ("LOOP" :foreground "#fabd2f")                                  ; recurring
          ("DELG" :foreground "#8ec07c")                                  ; delegated
          ("WAIT" :foreground "#fabd2f" :slant italic)                    ; waiting
          ("HOLD" :foreground "#7c6f64" :slant italic)                    ; on hold
          ("IDEA" :foreground "#d65d0e" :slant italic)                    ; idea

          ;; Done / Cancelled
          ("DONE" :foreground "#98971a")                                  ; done
          ("CNCL" :foreground "#928374" :strike-through t)               ; cancelled

          ;; Checkboxes
          ("[ ]" :foreground "#fb4934")                                   ; unchecked
          ("[-]" :foreground "#fabd2f")                                   ; partial
          ("[X]" :foreground "#98971a")                                   ; checked

          ;; Special indicators
          ("[!]" :foreground "#fb4934" :weight bold :inverse-video t)    ; urgent
          ("[?]" :foreground "#d79921" :weight bold)                      ; uncertain
          ("[i]" :foreground "#b16286" :slant italic)                     ; info

          ;; Binary questions
          ("Y/N" :foreground "#fe8019" :weight bold)                      ; question
          ("YES" :foreground "#b8bb26")                                   ; yes
          ("NOP" :foreground "#928374")                                   ; no
          )
        )
  )
#+end_src

* Cursor Beacon
Useful for the user, but also when you have someone over your shoulder trying to follow what is going on x)

#+begin_src emacs-lisp
;; Flash the point (cursor) when moving between window
(use-package! beacon
  :config
  (beacon-mode 1))
#+end_src

* Trash

#+begin_src emacs-lisp
;; Deleted file go to trash instead of been destroyed for ever... (rm -r / --do-it)
(setq delete-by-moving-to-trash t
      trash-directory "~/.local/share/Trash/files/")
#+end_src

* Email

** Org-Msg

#+begin_src emacs-lisp
(after! org-msg
  (setq mail-user-agent 'mu4e-user-agent)
  (require 'org-msg)
  (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t"
        org-msg-startup "hidestars indent inlineimages"
        org-msg-greeting-fmt "\nBonjour%s,\n\n"
        ;; org-msg-recipient-names '(("jeremy.compostella@gmail.com" . "Jérémy"))
        org-msg-greeting-name-limit 1
        org-msg-default-alternatives '((new		. (text html))
                                       (reply-to-html	. (text html))
                                       (reply-to-text	. (text)))
        org-msg-convert-citation t
        org-msg-signature "

Cdlt,

,#+begin_signature
--

,*Thierry Pouplier*

1319 rue Bergar, Laval, Qc, Canada \\\\
H7L 4Z7 \\\\
Tél. : +1 (450) 667-1884 \\\\
Cell. : +1 (514) 887-1674 \\\\
tpouplier@tdnde.com \\\\
www.tdnde.com \\\\
,#+end_signature")
  )
#+end_src

** Mu4e

#+begin_src emacs-lisp
(after! mu4e
  (setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter)
  (setq mu4e-contexts
        (list
         (make-mu4e-context
          :name "TDNDE"
          :match-func
          (lambda (msg)
            (when msg
              (string-prefix-p
               "/TDNDE/"
               (mu4e-message-field msg :maildir))))
          :vars
          '((user-mail-address . "tpouplier@tdnde.com")
            (user-full-name    . "Thierry Pouplier")

            ;; Folders
            (mu4e-sent-folder   . "/TDNDE/Sent")
            (mu4e-drafts-folder . "/TDNDE/Drafts")
            (mu4e-trash-folder  . "/TDNDE/Trash")
            (mu4e-refile-folder . "/TDNDE/Archive")

            ;; Shortcuts
            (mu4e-maildir-shortcuts
             .
             (("/TDNDE/Inbox"   . ?i)
              ("/TDNDE/Sent"    . ?s)
              ("/TDNDE/Drafts"  . ?d)
              ("/TDNDE/Trash"   . ?t)
              ("/TDNDE/Archive" . ?a)))

            ;; Bookmarks
            (mu4e-bookmarks
             .
             ((:name "Airbus-6825"
               :query "maildir:/TDNDE/* AND label:proj:6825 AND label:client:airbus"
               :key ?A)
              (:name "Bombardier-3154"
               :query "maildir:/TDNDE/* AND label:proj:3154 AND label:client:bombardier"
               :key ?B)
              (:name "Daher-5304"
               :query "maildir:/TDNDE/* AND label:proj:5304 AND label:client:daher"
               :key ?D)
              (:name "TDNDE"
               :query "maildir:/TDNDE/* AND label:tdnde"
               :key ?T)))

            ;; SMTP
            (smtpmail-smtp-user    . "tpouplier@tdnde.com")
            (smtpmail-stream-type  . ssl)
            (smtpmail-smtp-server  . "smtp.hostinger.com")
            (smtpmail-smtp-service . 465)))
         ;; Other context here
         ;; (make-mu4e-context ...)
         )
        )

  (setq mu4e-context-policy 'pick-first
        mu4e-compose-context-policy 'ask)
  
  (require 'mu4e-icalendar)
  (mu4e-icalendar-setup)

  (setq mu4e-icalendar-diary-file nil)

  (setq gnus-icalendar-org-capture-file +org-capture-journal-file
        gnus-icalendar-org-capture-headline '("Journal"))

  (gnus-icalendar-org-setup)
  )

(after! mu4e
  (setq mu4e-modeline-support nil))

(after! mu4e
  ;; restore label key
  (evil-define-key 'normal mu4e-headers-mode-map"l" #'mu4e-headers-mark-for-label)
  (evil-define-key 'visual mu4e-headers-mode-map "l" #'mu4e-headers-mark-for-label)
  (evil-define-key 'emacs mu4e-headers-mode-map "l" #'mu4e-headers-mark-for-label)

  (evil-define-key 'normal mu4e-view-mode-map "l" #'mu4e-view-mark-for-label)
  (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)
  )
#+end_src

* IRC
** Circe

#+begin_src emacs-lisp
(defun gortium-circe-nickserv-password (server)
  (auth-source-passage-get 'secret "irc"))

(after! circe
  ;; set your nick, username, and real name
  (setq circe-nick "gortium"
        circe-user-name "gortium"
        circe-real-name "gortium")
  (setq circe-network-options
        '(("Libera Chat"
           :tls t
           :nick "gortium"
           :sasl-username "gortium"
           :nickserv-password gortium-circe-nickserv-password
           :channels ("#emacs-circe")
           )))
  )
#+end_src

* Org-Noter

#+begin_src emacs-lisp :tangle no
;; Org-Noter
(use-package! org-noter
  :after org  ;; optional, ensures org is loaded first
  :config
  (setq org-noter-notes-search-path '("~/org/"))
  (setq org-noter-default-notes-file-names '("notes.org"))
  (setq org-noter-notes-window-location 'vertical-split)
  (setq org-noter-notes-window-behavior '(start scroll))
  (setq org-noter-doc-split-fraction '(0.5 . 0.8))
  (setq org-noter-always-create-frame nil)
  (setq org-noter-hide-other t)
  (setq org-noter-kill-frame-at-session-end nil))
#+end_src

** TODO Fix noter if needed
:PROPERTIES:
:ID:       150ae9d2-df33-41d2-9cc1-f22d95659fed
:END:

* PDF-Tools
Really useful to be able to have a buffer with notes, and another with the official PDF doc

#+begin_src emacs-lisp
;; Enable midnight mode by default for PDF files
(use-package! pdf-tools
  :magic ("%PDF" . pdf-view-mode)
  :config
  ;; Install the server binary if needed (permanent)
  (pdf-tools-install :no-query)

  ;; Hooks
  (add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)
  (add-hook 'pdf-view-mode-hook #'pdf-view-roll-minor-mode)
  )
#+end_src

* Custom Emacs-Everywhere for Hyprland

#+begin_src emacs-lisp
;; Like Emacs everywhere, but work in hyprland
(defun thanos/wtype-text (text)
  "Process TEXT for wtype, handling newlines properly."
  (let* ((has-final-newline (string-match-p "\n$" text))
         (lines (split-string text "\n"))
         (last-idx (1- (length lines))))
    (string-join
     (cl-loop for line in lines
              for i from 0
              collect (cond
                       ;; Last line without final newline
                       ((and (= i last-idx) (not has-final-newline))
                        (format "wtype -s 350 \"%s\""
                                (replace-regexp-in-string "\"" "\\\\\"" line)))
                       ;; Any other line
                       (t
                        (format "wtype -s 350 \"%s\" && wtype -k Return"
                                (replace-regexp-in-string "\"" "\\\\\"" line)))))
     " && ")))

(define-minor-mode thanos/type-mode
  "Minor mode for inserting text via wtype."
  :keymap `((,(kbd "C-c C-c") . ,(lambda () (interactive)
                                   (call-process-shell-command
                                    (thanos/wtype-text (buffer-string))
                                    nil 0)
                                   (delete-frame)))
            (,(kbd "C-c C-k") . ,(lambda () (interactive)
                                   (kill-buffer (current-buffer))))))

(defun thanos/type ()
  "Launch a temporary frame with a clean buffer for typing."
  (interactive)
  (let ((frame (make-frame '((name . "emacs-float")
                             (fullscreen . 0)
                             (undecorated . t)
                             (width . 70)
                             (height . 20))))
        (buf (get-buffer-create "emacs-float")))
    (select-frame frame)
    (switch-to-buffer buf)
    (with-current-buffer buf
      (erase-buffer)
      (org-mode)
      (flyspell-mode)
      (thanos/type-mode)
      (setq-local header-line-format
                  (format " %s to insert text or %s to cancel."
                          (propertize "C-c C-c" 'face 'help-key-binding)
			  (propertize "C-c C-k" 'face 'help-key-binding)))
      ;; Make the frame more temporary-like
      (set-frame-parameter frame 'delete-before-kill-buffer t)
      (set-window-dedicated-p (selected-window) t))))
#+end_src

* Custom function refile to datetree

#+begin_src emacs-lisp
;;Refile in datetree function
(defun org-refile-to-datetree (&optional file)
  "Refile a subtree to a datetree corresponding to its timestamp.

The current time is used if the entry has no timestamp.
If FILE is nil, refile in the current file."
  (interactive
   (list (read-file-name "Refile to file: "
                         nil                          ; directory
                         (buffer-file-name)           ; default filename
                         t)))                         ; mustmatch
  (let* ((datetree-date (or (org-entry-get nil "TIMESTAMP" t)
                            (org-read-date t nil "now")))
         (date (org-date-to-gregorian datetree-date)))
    (with-current-buffer (current-buffer)
      (save-excursion
        (org-cut-subtree)
        (when file
          (find-file file))
        (org-datetree-find-date-create date)
        (org-narrow-to-subtree)
        (show-subtree)
        (org-end-of-subtree t)
        (newline)
        (goto-char (point-max))
        (org-paste-subtree 4)
        (widen)))))
#+end_src

* Custom function ediff org fold

#+begin_src emacs-lisp
;; Open all org fold in ediff
(defun org-ediff-prepare-buffer ()
  (when (memq major-mode '(org-mode emacs-lisp-mode))
    (outline-show-all)))

(add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer)
#+end_src

* Custom function for reschedulling

#+begin_src emacs-lisp
;;; ============================================================
;;; GORTIUM — Org chain scheduler (Safe & Optimized with Debugging)
;;; ============================================================

(require 'org)
(require 'org-id)
(require 'cl-lib)
(require 's)

;; ------------------------------------------------------------
;; Helper: Skip weekends (snap to next weekday at 08:00)
;; ------------------------------------------------------------
(defun gortium/org--skip-weekend (time)
  (let* ((decoded (decode-time time))
         (dow (nth 6 decoded)))
    (cond
     ((= dow 6) ;; Saturday
      (apply #'encode-time
             (append '(0 0 8)
                     (nthcdr 3 (decode-time (time-add time (days-to-time 2)))))))
     ((= dow 0) ;; Sunday
      (apply #'encode-time
             (append '(0 0 8)
                     (nthcdr 3 (decode-time (time-add time (days-to-time 1)))))))
     (t time))))

;; ------------------------------------------------------------
;; Helper: Snap to working hours (08:00–16:00)
;; ------------------------------------------------------------
(defun gortium/internal--snap-to-working-hours (time)
  (let* ((day-start 8)
         (day-end 16)
         (t1 (gortium/org--skip-weekend time))
         (d (decode-time t1))
         (h (nth 2 d)))
    (cond
     ((< h day-start)
      (apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
     ((>= h day-end)
      (let ((next (time-add t1 (days-to-time 1))))
        (gortium/org--skip-weekend
         (apply #'encode-time (append (list 0 0 day-start)
                                      (nthcdr 3 (decode-time next)))))))
     (t t1))))

(defun gortium/org--get-range-end (pos)
  "Extract the END timestamp from an existing range like <A>--<B> at POS.
POS can be a marker or a cons cell (file . position)."
  (let ((marker (if (markerp pos) pos 
                  (let ((file (car pos))
                        (p (cdr pos)))
                    (with-current-buffer (find-file-noselect file)
                      (copy-marker p))))))
    (with-current-buffer (marker-buffer marker)
      (save-excursion
        (goto-char marker)
        (org-back-to-heading t)
        (let ((subtree-end (save-excursion (org-end-of-subtree t) (point)))
              (end-time nil))
          (save-restriction
            (narrow-to-region (point) subtree-end)
            (goto-char (point-min))
            (when (re-search-forward "<[^>]+>--<\\([^>]+\\)>" nil t)
              (condition-case nil
                  (setq end-time (org-time-string-to-time (match-string 1)))
                (error nil))))
          end-time)))))

(defun gortium/org--get-range-start (pos)
  "Extract the start timestamp from an existing range like <A>--<B>.
Returns nil if no range found (safe, non-blocking)."
  (save-excursion
    (goto-char pos)
    (org-back-to-heading t)
    (let ((end (save-excursion 
                 (or (ignore-errors (outline-next-heading))
                     (point-max))
                 (point)))
          (start-time nil))
      (save-restriction
        (narrow-to-region (point) end)
        (goto-char (point-min))
        ;; Look for range in first 50 lines only (safety limit)
        (let ((search-limit (save-excursion 
                              (forward-line 50)
                              (point))))
          (when (re-search-forward "<\\([^>]+\\)>--<[^>]+>" search-limit t)
            (condition-case nil
                (setq start-time (org-time-string-to-time (match-string 1)))
              (error nil)))))
      start-time)))

(defun gortium/internal--calculate-task-span (start-time effort-str)
  "Return a list (END-TIME WEEKEND-DAYS) for given START-TIME and EFFORT string."
  (if (or (null effort-str) (string-empty-p effort-str))
      (list start-time 0)
    (let* ((eff-mins (org-duration-to-minutes effort-str))
           (total-work-mins (if (string-match-p "d" effort-str)
                                (* (/ eff-mins 1440.0) 480)
                              eff-mins))
           (cursor start-time)
           (wknd-count 0)
           (day-start 8)
           (day-end 16)
           (safety-counter 0)
           (max-iterations 1000)) ;; Safety limit
      (while (and (> total-work-mins 0) (< safety-counter max-iterations))
        (setq safety-counter (1+ safety-counter))
        (let* ((decoded (decode-time cursor))
               (h (nth 2 decoded))
               (m (nth 1 decoded))
               (dow (nth 6 decoded))
               (current-abs-min (+ (* h 60) m))
               (day-end-abs-min (* day-end 60))
               (mins-left-today (- day-end-abs-min current-abs-min)))
          (cond
           ((or (= dow 6) (= dow 0)) ;; weekend
            (setq wknd-count (1+ wknd-count))
            (setq cursor (apply #'encode-time (append (list 0 0 day-start)
                                                      (nthcdr 3 (decode-time
                                                                 (time-add cursor (days-to-time 1))))))))
           ((<= mins-left-today 0) ;; after hours
            (setq cursor (apply #'encode-time (append (list 0 0 day-start)
                                                      (nthcdr 3 (decode-time
                                                                 (time-add cursor (days-to-time 1))))))))
           ((<= total-work-mins mins-left-today)
            (setq cursor (time-add cursor (seconds-to-time (* total-work-mins 60))))
            (setq total-work-mins 0))
           (t ;; spill to next day
            (setq total-work-mins (- total-work-mins mins-left-today))
            (setq cursor (time-add cursor (seconds-to-time (* mins-left-today 60))))))))
      
      (when (>= safety-counter max-iterations)
        (message "WARNING: calculate-task-span hit iteration limit for effort %s" effort-str))
      
      (list cursor wknd-count))))

;; ------------------------------------------------------------
;; Helper: Find dependency end time
;; ------------------------------------------------------------
(defun gortium/internal--get-blocker-end (blocker-str task-end-map)
  "Return the latest end time of all blockers if they are all resolved."
  (let ((clean (s-trim (format "%s" blocker-str)))
        (latest-time nil)
        (all-resolved t))
    (when (and (string-match "ids(\\(.*?\\))" clean)
               (not (string-empty-p (s-trim (match-string 1 clean)))))
      (dolist (tid (split-string (match-string 1 clean) "[[:space:],]+" t))
        (let* ((clean-id (replace-regexp-in-string "[\"']\\|id: " "" tid))
               (pos (org-id-find clean-id t))
               (computed-end (gethash clean-id task-end-map))
               (blocker-end nil))
          
          ;; 1. Determine this specific blocker's end time
          (setq blocker-end
                (cond
                 (computed-end computed-end) ;; Use what we just calculated in this session
                 (pos (let ((m (if (markerp pos) pos 
                                 (set-marker (make-marker) (cdr pos) 
                                             (find-file-noselect (car pos))))))
                        (with-current-buffer (marker-buffer m)
                          (org-with-point-at m
                            (cond
                             ;; Priority 1: Use actual CLOSED timestamp if DONE
                             ((org-entry-get nil "CLOSED")
                              (org-time-string-to-time (org-entry-get nil "CLOSED")))
                             ;; Priority 2: Use range end if FIXED
                             ((string-equal "t" (org-entry-get nil "FIXED"))
                              (gortium/org--get-range-end m))
                             ;; Priority 3: Fallback to existing range end if it exists
                             ((gortium/org--get-range-end m)
                              (gortium/org--get-range-end m))
                             (t nil))))))
                 (t nil)))

          ;; 2. Update the "Latest" tracker
          (if blocker-end
              (when (or (null latest-time) (time-less-p latest-time blocker-end))
                (setq latest-time blocker-end))
            ;; If ANY blocker is not resolved/found, the whole task is not ready
            (setq all-resolved nil)))))
    
    ;; Only return a time if EVERY ID in the list was successfully resolved
    (when all-resolved latest-time)))

(defun gortium/internal--update-properties (pos start wknd id end task-end-map)
  "Heals the property drawer, updates values, and fixes vertical spacing without leaking newlines."
  (message "[DEBUG] enter update-properties for %s" id)
  (save-excursion
    (goto-char pos)
    (org-back-to-heading t)
    (let* ((subtree-start (point))
           (subtree-end (save-excursion (org-end-of-subtree t) (point))))

      (save-restriction
        (narrow-to-region subtree-start subtree-end)

        ;; --- STEP 1: HEAL THE DRAWER ---
        (goto-char (point-min))
        (forward-line 1)
        (while (looking-at "^[ \t]*\\(CLOSED:\\|SCHEDULED:\\|DEADLINE:\\)")
          (forward-line 1))

        (when (looking-at "^[ \t]*:PROPERTIES:[ \t]*$")
          (let ((drawer-start (point)))
            (when (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)
              (let ((drawer-end (match-end 0)))
                (save-restriction
                  (narrow-to-region drawer-start drawer-end)
                  (goto-char (point-min))
                  (forward-line 1)
                  (while (re-search-forward "^[ \t]*$" nil t)
                    (delete-region (line-beginning-position)
                                   (min (1+ (line-end-position)) (point-max)))))))))

        ;; --- STEP 2: UPDATE PROPERTY ---
        (org-entry-put nil "WEEKEND_DAYS" (number-to-string (or wknd 0)))

        ;; --- STEP 3: REMOVE OLD RANGE ---
        (goto-char (point-min))
        (while (re-search-forward "^[ \t]*<.+>--<.+>[ \t]*\n?" nil t)
          (replace-match ""))

        ;; --- STEP 4: FIND INSERTION POINT ---
        (goto-char (point-min))
        (forward-line 1)
        (while (looking-at "^[ \t]*\\(CLOSED:\\|SCHEDULED:\\|DEADLINE:\\)")
          (forward-line 1))
        (while (looking-at "^[ \t]*:\\([A-Z_]+\\):[ \t]*$")
          (when (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)
            (forward-line 1)))
        
        ;; Delete any existing blank lines at the insertion point
        (while (and (looking-at "^[ \t]*$") (not (eobp)))
          (delete-region (line-beginning-position) (line-beginning-position 2)))

        ;; --- STEP 5: INSERT RANGE ---
        ;; Ensure range starts on a new line and ends with exactly one newline
        (unless (bolp) (insert "\n"))
        (insert (format "<%s>--<%s>\n"
                        (format-time-string "%Y-%m-%d %a %H:%M" start)
                        (format-time-string "%Y-%m-%d %a %H:%M" end)))

        ;; --- STEP 6: CLEAN UP REMAINING WHITESPACE WITHIN TASK ---
        (while (and (looking-at "^[ \t]*$") (not (eobp)))
          (delete-region (line-beginning-position) (line-beginning-position 2))))

      ;; --- STEP 7: NORMALIZE SPACING BETWEEN TASKS ---
      ;; Instead of inserting a newline blindly, we ensure exactly one blank line 
      ;; exists between this subtree's end and the next heading.
      (goto-char (org-end-of-subtree t))
      (let ((post-subtree (point)))
        (save-restriction
          (widen)
          (goto-char post-subtree)
          (delete-blank-lines)
          ;; Only insert a blank line if we aren't at the end of the buffer
          (unless (eobp)
            (insert "\n")))))

    (message "[DEBUG] exit update-properties")
    (puthash id end task-end-map)))

;; ------------------------------------------------------------
;; Helper: Detect circular dependencies
;; ------------------------------------------------------------
(defun gortium/internal--detect-circular-deps (tasks)
  "Check for circular dependencies in TASKS.
Returns list of task IDs involved in cycles, or nil if no cycles found."
  (let ((graph (make-hash-table :test 'equal))
        (visiting (make-hash-table :test 'equal))
        (visited (make-hash-table :test 'equal))
        (cycles nil))
    
    ;; Build dependency graph
    (dolist (task tasks)
      (pcase-let ((`(,_pos ,id ,_effort ,blocker ,_fixed ,_sched ,_rng-start ,_offset ,_state) task))
        (when (and blocker (not (string-empty-p (s-trim blocker))))
          (when (string-match "ids(\\(.*?\\))" blocker)
            (let ((deps (split-string (match-string 1 blocker) "[ ,]+" t)))
              (puthash id (mapcar (lambda (tid)
                                    (replace-regexp-in-string "[\"']\\|id:" "" tid))
                                  deps)
                       graph))))))
    
    ;; DFS to detect cycles
    (cl-labels ((dfs (node path)
                  (cond
                   ((gethash node visiting)
                    ;; Found a cycle
                    (push node cycles)
                    t)
                   ((gethash node visited)
                    nil)
                   (t
                    (puthash node t visiting)
                    (dolist (dep (gethash node graph))
                      (when (dfs dep (cons node path))
                        (push node cycles)))
                    (remhash node visiting)
                    (puthash node t visited)
                    nil))))
      
      (maphash (lambda (node _deps)
                 (unless (gethash node visited)
                   (dfs node nil)))
               graph))
    
    (delete-dups cycles)))

(advice-add 'org-roam-db-sync :before
            (lambda (&rest _)
              (message "[DEBUG] org-roam-db-sync invoked")))

;; --- MAIN SCHEDULER ---
(defun gortium/org-schedule-subtree-chains ()
  "Standard Gortium scheduler: Correctly calculates Finish-to-Start dependencies."
  (interactive)
  (message "=== Starting Gortium Scheduler ===")
  
  (let ((all-tasks '())
        (task-end-times (make-hash-table :test 'equal))
        (start-time (current-time))
        (org-element-use-cache nil)) ;; Disable buggy cache

    ;; 1. COLLECT
    (org-map-entries
     (lambda ()
       (when (org-get-todo-state)
         (let* ((pos (point-marker))
                (id (or (org-id-get) (org-id-get-create))))
           (push (list (current-buffer) pos id 
                       (org-entry-get pos "EFFORT")
                       (org-entry-get pos "BLOCKER")
                       (org-entry-get pos "FIXED")
                       (org-get-scheduled-time pos)
                       (org-entry-get pos "OFFSET_DAYS"))
                 all-tasks))))
     nil nil)

    (setq all-tasks (nreverse all-tasks))

    ;; 2. ITERATE
    (let* ((remaining all-tasks)
           (limit (* 20 (length remaining)))
           (iter 0))
      
      (while (and remaining (< iter limit))
        (setq iter (1+ iter))
        (let ((done-this-loop '()))
          (dolist (task remaining)
            (pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task))
              (let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times))
                     (has-blocker (and blocker (not (string-empty-p (s-trim blocker)))))
                     (is-fixed (string-equal fixed "t"))
                     (ready (or is-fixed (not has-blocker) blocker-end)))

                (when ready
                  (with-current-buffer buf
                    (org-element-with-disabled-cache
                      (let* ((off-days (if (stringp offset) (string-to-number offset) 0))
                             (base-start (cond 
                                          ;; 1. If FIXED, use its own defined start
                                          (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
                                          ;; 2. If it HAS a blocker, it MUST use blocker-end. 
                                          ;; If blocker-end is nil, this task isn't 'ready' yet.
                                          (has-blocker blocker-end)
                                          ;; 3. If no blocker and not fixed, use current schedule or now
                                          (t (or sched (current-time)))))
                             
                             (final-start (if is-fixed base-start
                                            (gortium/internal--snap-to-working-hours 
                                             (time-add base-start (days-to-time off-days)))))
                             
                             (span (gortium/internal--calculate-task-span final-start effort))
                             (final-end (car span))
                             (wknd (cadr span)))
                        
                        (gortium/internal--update-properties pos final-start wknd id final-end task-end-times)
                        (push task done-this-loop))))))))
          (setq remaining (cl-set-difference remaining done-this-loop))))

      ;; --- DIAGNOSTICS FOR UNSCHEDULED TASKS ---
      (if remaining
          (progn
            (message "!!! WARNING: %d tasks were NOT scheduled !!!" (length remaining))
            (dolist (task remaining)
              (pcase-let ((`(,_buf ,pos ,id ,_eff ,blocker ,_fix ,_sch ,_off) task))
                (let* ((blocker-string (format "%s" blocker))
                       (clean-blocker (if (string-match "ids(\\(.*?\\))" blocker-string)
                                          (match-string 1 blocker-string)
                                        "None")))
                  (message "  -> Task [%s] at pos %s is BLOCKED by: %s"
                           id (marker-position pos) clean-blocker)))))
        (message "=== All tasks scheduled successfully! ==="))

      ;; 3. CLEANUP
      (setq org-element-use-cache t)
      (org-element-cache-reset 'all)
      (message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter)
      ;; (gortium/org-rollup-parent-ranges)
      ;; (message "=== All Scheduling and Roll-ups Complete ===")
      )))

;; ---------------------------------------------
(defun gortium/org-ensure-task-properties ()
  "Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)
and ensure the standard property drawer exists without overwriting existing data."
  (interactive)
  (save-excursion
    (message "--- Initializing Task Properties for all states ---")
    (let ((count 0)
          ;; List of properties to ensure exist
          (props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS" 
                   "ASSIGNEE" "RESOURCES" "CATEGORY" 
                   "DIMENTIONS" "WEIGHT" "OFFSET_DAYS")))
      (org-map-entries
       (lambda ()
         ;; This check returns true if the heading has ANY todo keyword
         (let ((todo-state (org-get-todo-state)))
           (when todo-state
             (cl-incf count)
             ;; 1. Handle ID (builtin handles 'do not replace' logic)
             (org-id-get-create)
             
             ;; 2. Ensure all other keys exist
             (dolist (prop props)
               ;; Only add the property if it's currently nil/missing
               (unless (org-entry-get (point) prop)
                 (org-entry-put (point) prop 
                                (if (string= prop "FIXED") "nil" "")))))))
       nil nil)
      (message "--- Finished: Processed %d tasks across all states ---" count))))

(defun gortium/add-ids-to-subtree ()
  "Add IDs to all headings in current subtree."
  (interactive)
  (save-excursion
    (org-back-to-heading t)
    (org-map-entries
     (lambda () (org-id-get-create))
     nil 'tree)))
#+end_src

#+RESULTS:
: gortium/add-ids-to-subtree

* Custom function Open link in other frame

#+begin_src emacs-lisp
;; Open link in another frame
(defun gortium/org-open-link-in-other-frame ()
  "Open the Org link at point in another frame."
  (interactive)
  (let ((org-link-frame-setup '((file . find-file-other-frame)
                                (id . find-file-other-frame))))
    (org-open-at-point)))

(map! :after org
      :map org-mode-map
      :n "gF" #'gortium/org-open-link-in-other-frame)
#+end_src

* Custom function to open current buffer to new frame

#+begin_src emacs-lisp
(defvar gortium/main-frame nil
  "The main Emacs frame where buffers should be toggled to/from.")

(add-hook 'emacs-startup-hook
          (lambda () (setq gortium/main-frame (selected-frame))))

(defun gortium--rightmost-non-minibuffer-window (frame)
  "Return the rightmost non-minibuffer window in FRAME, or nil if none."
  (let ((best-win nil)
        (best-right -1))
    (dolist (w (window-list frame))
      (unless (window-minibuffer-p w)
        (let ((right (nth 2 (window-edges w))))
          (when (> right best-right)
            (setq best-right right
                  best-win w)))))
    best-win))

(defun gortium--non-minibuffer-window-count (frame)
  "Return number of non-minibuffer windows in FRAME."
  (let ((count 0))
    (dolist (w (window-list frame))
      (unless (window-minibuffer-p w)
        (setq count (1+ count))))
    count))

(defun gortium/toggle-buffer-to-frame ()
  "Toggle the current buffer between `gortium/main-frame' and a new frame."
  (interactive)
  (unless (frame-live-p gortium/main-frame)
    (setq gortium/main-frame (selected-frame)))

  (let* ((buf (current-buffer))
         (old-frame (selected-frame))
         (old-win (selected-window)))
    (if (eq old-frame gortium/main-frame)
        ;; === Move from main frame to new frame ===
        (let ((new-frame (make-frame '((name . "Toggled Buffer")))))
          (with-selected-frame new-frame
            (switch-to-buffer buf))
          (select-frame-set-input-focus new-frame)
          (with-selected-frame old-frame
            (with-selected-window old-win
              (if (> (gortium--non-minibuffer-window-count old-frame) 1)
                  (delete-window old-win)
                (switch-to-buffer (other-buffer buf t))))))
      ;; === Move from secondary frame back to main ===
      (with-selected-frame gortium/main-frame
        (let ((target (gortium--rightmost-non-minibuffer-window gortium/main-frame)))
          (if target
              (with-selected-window target
                (condition-case nil
                    (let ((new-win (split-window-right)))
                      (select-window new-win)
                      (switch-to-buffer buf))
                  (error ;; too small to split, just reuse the window
                   (switch-to-buffer buf))))
            ;; fallback: no window found, just use current one
            (switch-to-buffer buf))))

      ;; Close secondary frame or just the window
      (with-selected-frame old-frame
        (if (= (gortium--non-minibuffer-window-count old-frame) 1)
            (delete-frame old-frame)
          (when (window-live-p old-win)
            (delete-window old-win)))))))
#+end_src

* Custom function for ExoKortex

#+begin_src emacs-lisp
;; >=== ExoKortex System ===<

(defvar gortium/org-repo-projects "~/ExoKortex/2-Areas/Meta-Planning/Org/Projects"
  "Path to your main Org repository where real .org files are stored.")

(defvar gortium/projects-root "~/ExoKortex/1-Projects/"
  "Root directory where new projects will be created.")

(defun gortium/create-project (project-name)
  "Create a new project with PROJECT-NAME in `gortium/projects-root`.
The .org file is created in `gortium/org-repo-projects` and symlinked into the project folder."
  (interactive "sProject name: ")
  (let* ((project-dir (expand-file-name project-name gortium/projects-root))
         (org-file (concat project-name ".org"))
         (org-real (expand-file-name org-file gortium/org-repo-projects))
         (org-link (expand-file-name org-file project-dir)))
    (make-directory project-dir t)
    (unless (file-exists-p org-real)
      (with-temp-file org-real
        (insert (format "#+TITLE: %s\n#+DATE: %s\n\n" project-name (format-time-string "%Y-%m-%d")))))
    (make-symbolic-link org-real org-link t)
    (find-file org-link)))

(defun gortium/create-symlinked-org (file-name)
  "Create a symlinked org FILE-NAME in current dir, real file in `gortium/org-repo-projoects`."
  (interactive "sOrg file name (without .org): ")
  (let* ((org-file (concat file-name ".org"))
         (org-real (expand-file-name org-file gortium/org-repo-projects))
         (org-link (expand-file-name org-file default-directory)))
    (unless (file-exists-p org-real)
      (with-temp-file org-real
        (insert (format "#+TITLE: %s\n#+DATE: %s\n\n" file-name (format-time-string "%Y-%m-%d")))))
    (make-symbolic-link org-real org-link t)
    (find-file org-link)))

(defun gortium/convert-marked-org-to-symlink ()
  "Convert all marked org files in Dirvish/Dired to symlinks in `gortium/org-repo-projects`."
  (interactive)
  (let ((files (dired-get-marked-files)))
    (dolist (file-path files)
      (when (and (file-regular-p file-path)
                 (string= (file-name-extension file-path) "org"))
        (let* ((file-name (file-name-nondirectory file-path))
               (org-real (expand-file-name file-name gortium/org-repo-projects)))
          (unless (file-exists-p org-real)
            (rename-file file-path org-real))
          (make-symbolic-link org-real file-path t)
          (message "Converted %s to symlink -> %s" file-path org-real))))))
#+end_src

* Custom function to refile task to dailies

#+begin_src emacs-lisp
(defun gortium/refile-to-today-daily ()
  "Refile the current subtree under the 'Journal' headline in today's daily note.
This version uses `org-roam-dailies-capture-today` to ensure the daily note
is created with Org-roam’s templates (including :ID:)."
  (interactive)
  (require 'org-roam-dailies)

  (let ((headline "Journal"))

    ;; Step 1: Ensure today’s daily exists via org-roam (guarantees :ID:)
    (let ((daily-file (org-roam-dailies-capture-today nil)))
      ;; Step 2: Cut the subtree (use org-copy-subtree if you prefer duplication)
      (org-cut-subtree)

      ;; Step 3: Paste under the Journal headline
      (with-current-buffer (find-file-noselect daily-file)
        (save-excursion
          (goto-char (point-min))
          (when (re-search-forward (concat "^\\*+ " (regexp-quote headline) "\\b") nil t)
            (let ((parent-level (org-current-level)))
              (end-of-line)
              (insert "\n")
              (org-paste-subtree (+ 1 parent-level))))))
      (message "Task refiled under '%s' in today’s daily note." headline))))
#+end_src

#+begin_src emacs-lisp
(defun gortium/refile-to-daily-of-close ()
  "Refile the current subtree under the 'Journal' headline in the daily note
corresponding to the task's CLOSED date. If no CLOSED property is found,
fallback to today's daily note. Ensures the daily has an Org-roam ID."
  (interactive)
  (require 'org-roam-dailies)

  ;; Step 1: Get CLOSED property timestamp
  (let* ((closed-prop (org-entry-get (point) "CLOSED"))
         (time (when closed-prop
                 (ignore-errors (org-time-string-to-time closed-prop))))
         (date (or time (current-time)))
         (headline "Journal"))

    ;; Step 2: Ensure daily file exists using org-roam (this guarantees :ID: etc.)
    (let ((daily-file (org-roam-dailies-capture-date date nil)))
      ;; Step 3: Cut the subtree.
      (org-cut-subtree)

      ;; Step 4: Paste into the proper daily file
      (with-current-buffer (find-file-noselect daily-file)
        (save-excursion
          (goto-char (point-min))
          ;; Find the 'Journal' headline
          (when (re-search-forward (concat "^\\*+ " (regexp-quote headline) "\\b") nil t)
            (let ((parent-level (org-current-level)))
              (end-of-line)
              (insert "\n")
              (org-paste-subtree (+ 1 parent-level))))))

      (message "Task refiled under '%s' in %s"
               headline
               (file-name-nondirectory daily-file)))))
#+end_src
* LifeOS

WIP, deactivated for now.
#+begin_src emacs-lisp :tangle no
;; Define the path to the root of your project repository.
(defvar lifeos-project-dir "/home/tpouplier/ExoKortex/1-Projects/Perso/lifeOS")

(defvar lifeos-org-directory "~/org/"
  "The root directory for the LifeOS org files.")

;; Construct the path to the sub-directory containing your .el files.
(defvar lifeos-elisp-dir (expand-file-name "emacs/" lifeos-project-dir))

;; Add this directory to Emacs's load-path so it can find your file.
(add-to-list 'load-path lifeos-elisp-dir)

;; Load the library using `require`.
(require 'lifeos)
#+end_src

* +Whisper+

#+begin_src emacs-lisp :tangle no
(use-package! whisper
  :load-path "/home/tpouplier/3-Resources/whisper.el"
  :bind ("C-H-r" . whisper-run)
  :config
  (setq whisper-install-directory "/tmp/"
        whisper-model "base"
        whisper-language "auto"
        whisper-translate nil
        whisper-use-threads (/ (num-processors) 2))
  (setq whisper--ffmpeg-input-device "pulse:whisper-combined.monitor"))
#+end_src

* +Emacs-Everywhere+

#+begin_src emacs-lisp :tangle no
(after! emacs-everywhere
  (setq emacs-everywhere-window-focus-command (list "hyprctl" "dispatch" "focuswindow" "address:%w"))
  (setq emacs-everywhere-app-info-function #'emacs-everywhere--app-info-linux-hyprland)

  (require 'json)
  (defun emacs-everywhere--app-info-linux-hyprland ()
    "Return information on the current active window, on a Linux Hyprland session."
    (let* ((json-string (emacs-everywhere--call "hyprctl" "-j" "activewindow"))
           (json-object (json-read-from-string json-string))
           (window-id (cdr (assoc 'address json-object)))
           (app-name (cdr (assoc 'class json-object)))
           (window-title (cdr (assoc 'title json-object)))
           (window-geometry (list (aref (cdr (assoc 'at json-object)) 0)
                                  (aref (cdr (assoc 'at json-object)) 1)
                                  (aref (cdr (assoc 'size json-object)) 0)
                                  (aref (cdr (assoc 'size json-object)) 1))))
      (make-emacs-everywhere-app
       :id window-id
       :class app-name
       :title window-title
       :geometry window-geometry)))
  )
#+end_src

* +Smerge+

#+begin_src emacs-lisp :tangle no
;; Smerge remap in magit
Not needed
(after! magit
  (map! :after magit
        :leader
        :desc "Keep upper" "m u" #'magit-smerge-keep-upper
        :desc "Keep lower" "m l" #'magit-smerge-keep-lower
        :desc "Keep all"   "m a" #'magit-smerge-keep-all))

;; Smerge function to keep one side for the whole file
;; Not working.. used this instead:
;; git checkout --theirs <filename>
;; or
;; git checkout --ours <filename>
(defun gortium/smerge-keep-lower-all ()
  "Keep the lower part in all smerge conflict hunks in the current buffer."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (not (null (smerge-at-markers)))
      (smerge-keep-lower)
      (ignore-errors (smerge-next)))))

(defun gortium/smerge-keep-upper-all ()
  "Keep the upper part in all smerge conflict hunks in the current buffer."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (not (null (smerge-at-markers)))
      (smerge-keep-upper)
      (ignore-errors (smerge-next)))))
#+end_src

* +Excalidraw+

#+begin_src emacs-lisp :tangle no
Need chrome... :(
(use-package! org-excalidraw
  :config
  (setq org-excalidraw-directory "~/org/drawings"))
#+end_src

* +Org-Edna+

#+begin_src emacs-lisp :tangle no
;; Org-edna for advanced task dependencies
(use-package! org-edna
  :after org
  :config
  (setq org-edna-use-inheritance t)
  (org-edna-mode 1)
  (map! :map org-mode-map
        :localleader
        :desc "Edit Edna rules" "E" #'org-edna-edit))
#+end_src

