Fixed multi blocker end time. WIP on broken elgantt

This commit is contained in:
2026-02-11 16:46:03 -05:00
parent dfef1e6dfa
commit ddbc923314
2 changed files with 273 additions and 490 deletions

View File

@@ -571,227 +571,219 @@ fc --> UC3
[[/tmp/babel-pg1Nry/plantuml-SHtP1g.png][/tmp/babel-pg1Nry/plantuml-SHtP1g.png]]
* Elgantt
#+begin_src emacs-lisp
;; (require 'cl-lib)
;; (require 'dash)
;; (unless (fboundp 'first) (defalias 'first #'car))
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)
;; 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.")
(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/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))
(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)
(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)))))
;; 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."))))
;; 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)
;; 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)))
(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-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-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)
(setq elgantt-user-set-color-priority-counter 0)
;; (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 ;; <-- FIX: Removed (1+ ...) to stop overshoot
;; `(priority ,(setq elgantt-user-set-color-priority-counter
;; (1- elgantt-user-set-color-priority-counter))
;; :elgantt-user-overlay ,elgantt-org-id))))))))
;; --- 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)))))))
;; ;; --- 2. Effort Rule (With Weekend Extension) ---
;; ;; (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))
;; ;; ;; Add the weekend jump days to the visual length
;; ;; (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))
;; ;; ;; FIX 1: compute p2 by date (handles "|" separators)
;; ;; ;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot
;; ;; (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 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)))))))))))
;; (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)))
;; ;; 1. GET DESTINATION (Start of current task)
;; ;; We use the built-in elgantt-scheduled arg if available, it's faster and safer.
;; (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))))))
;; --- 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")))))))))))))))))
;; (when (numberp p-dest)
;; (let ((ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw))
;; (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)))
;; ;; 2. GET BLOCKER END DATE
;; (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))))))))))
;; ;; 3. DRAW
;; (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")
;; ;; Force to row if it jumped
;; (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!")))
(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:
@@ -2351,36 +2343,38 @@ Returns nil if no range found (safe, non-blocking)."
(when (and (string-match "ids(\\(.*?\\))" clean)
(not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid))
(pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos)
(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
(blocker-end nil))
;; 1. Determine this specific blocker's end time
(setq blocker-end
(cond
(computed-end computed-end)
;; Use a temporary buffer context to check properties if pos is a list
((and 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
((org-entry-get nil "CLOSED")
(org-time-string-to-time (org-entry-get nil "CLOSED")))
((string-equal "t" (org-entry-get nil "FIXED"))
(gortium/org--get-range-end m))
(t nil))))))
;; This captures the result of the let/with-current-buffer block
(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
(if (org-entry-get nil "CLOSED")
(org-time-string-to-time (org-entry-get nil "CLOSED"))
(gortium/org--get-range-end m))))))
(t nil))))
(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))
(t nil))))))
(t nil)))
;; 2. Update the "Latest" tracker
(if blocker-end
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end))
blocker-end latest-time))
(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)
@@ -2458,8 +2452,8 @@ Returns nil if no range found (safe, non-blocking)."
(unless (eobp)
(insert "\n")))))
(message "[DEBUG] exit update-properties")
(puthash id end task-end-map)))
(message "[DEBUG] exit update-properties")
(puthash id end task-end-map)))
;; ------------------------------------------------------------
;; Helper: Detect circular dependencies
@@ -2559,10 +2553,14 @@ Returns list of task IDs involved in cycles, or nil if no cycles found."
(with-current-buffer buf
(org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0))
;; CRITICAL: For fixed tasks, base-start is THEIR start.
;; For dependent tasks, base-start is the blocker's END.
(base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
(t (or blocker-end (or sched (current-time))))))
(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

View File

@@ -400,227 +400,6 @@
;; Enable plantuml-mode for PlantUML files
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
;; (require 'cl-lib)
;; (require 'dash)
;; (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)
;; (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 ;; <-- FIX: Removed (1+ ...) to stop overshoot
;; `(priority ,(setq elgantt-user-set-color-priority-counter
;; (1- elgantt-user-set-color-priority-counter))
;; :elgantt-user-overlay ,elgantt-org-id))))))))
;; ;; --- 2. Effort Rule (With Weekend Extension) ---
;; ;; (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))
;; ;; ;; Add the weekend jump days to the visual length
;; ;; (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))
;; ;; ;; FIX 1: compute p2 by date (handles "|" separators)
;; ;; ;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot
;; ;; (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))))))))))
;; (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)))
;; ;; 1. GET DESTINATION (Start of current task)
;; ;; We use the built-in elgantt-scheduled arg if available, it's faster and safer.
;; (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 ((ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw))
;; (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)))
;; ;; 2. GET BLOCKER END DATE
;; (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))))))))))
;; ;; 3. DRAW
;; (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")
;; ;; Force to row if it jumped
;; (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!")))
(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/"
@@ -1697,36 +1476,38 @@ Returns nil if no range found (safe, non-blocking)."
(when (and (string-match "ids(\\(.*?\\))" clean)
(not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid))
(pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos)
(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
(blocker-end nil))
;; 1. Determine this specific blocker's end time
(setq blocker-end
(cond
(computed-end computed-end)
;; Use a temporary buffer context to check properties if pos is a list
((and 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
((org-entry-get nil "CLOSED")
(org-time-string-to-time (org-entry-get nil "CLOSED")))
((string-equal "t" (org-entry-get nil "FIXED"))
(gortium/org--get-range-end m))
(t nil))))))
;; This captures the result of the let/with-current-buffer block
(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
(if (org-entry-get nil "CLOSED")
(org-time-string-to-time (org-entry-get nil "CLOSED"))
(gortium/org--get-range-end m))))))
(t nil))))
(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))
(t nil))))))
(t nil)))
;; 2. Update the "Latest" tracker
(if blocker-end
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end))
blocker-end latest-time))
(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)
@@ -1804,8 +1585,8 @@ Returns nil if no range found (safe, non-blocking)."
(unless (eobp)
(insert "\n")))))
(message "[DEBUG] exit update-properties")
(puthash id end task-end-map)))
(message "[DEBUG] exit update-properties")
(puthash id end task-end-map)))
;; ------------------------------------------------------------
;; Helper: Detect circular dependencies
@@ -1905,10 +1686,14 @@ Returns list of task IDs involved in cycles, or nil if no cycles found."
(with-current-buffer buf
(org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0))
;; CRITICAL: For fixed tasks, base-start is THEIR start.
;; For dependent tasks, base-start is the blocker's END.
(base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
(t (or blocker-end (or sched (current-time))))))
(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