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]] [[/tmp/babel-pg1Nry/plantuml-SHtP1g.png][/tmp/babel-pg1Nry/plantuml-SHtP1g.png]]
* Elgantt * Elgantt
#+begin_src emacs-lisp Broken for now..
;; (require 'cl-lib) #+begin_src emacs-lisp :tangle no
;; (require 'dash) (require 'cl-lib)
;; (unless (fboundp 'first) (defalias 'first #'car)) (require 'dash)
(require 'elgantt)
(unless (fboundp 'first) (defalias 'first #'car))
;; ;; Clear rules to ensure the new global color logic takes effect immediately ;; Clear rules to ensure the new global color logic takes effect immediately
;; (setq elgantt--display-rules nil) (setq elgantt--display-rules nil)
;; (defface gortium/elgantt-weekend-face (defface gortium/elgantt-weekend-face
;; '((t (:background "#32302f" :extend nil))) '((t (:background "#32302f" :extend nil)))
;; "Gruvbox Dark0_Hard/Soft mix for subtle weekend stripes.") "Gruvbox Dark0_Hard/Soft mix for subtle weekend stripes.")
;; (defun gortium/internal--month-to-num (name) (defun gortium/internal--month-to-num (name)
;; "Convert month string to number safely." "Convert month string to number safely."
;; (let ((case-fold-search t)) (let ((case-fold-search t))
;; (cond ((string-match-p "Jan" name) 1) ((string-match-p "Feb" name) 2) (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 "Mar" name) 3) ((string-match-p "Apr" name) 4)
;; ((string-match-p "May" name) 5) ((string-match-p "Jun" name) 6) ((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 "Jul" name) 7) ((string-match-p "Aug" name) 8)
;; ((string-match-p "Sep" name) 9) ((string-match-p "Oct" name) 10) ((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)))) ((string-match-p "Nov" name) 11) ((string-match-p "Dec" name) 12) (t 1))))
;; (defun gortium/elgantt-draw-weekend-guides () (defun gortium/elgantt-draw-weekend-guides ()
;; "Draw weekend guides for the ENTIRE buffer once to prevent scroll lag." "Draw weekend guides for the ENTIRE buffer once to prevent scroll lag."
;; (interactive) (interactive)
;; (when (derived-mode-p 'elgantt-mode) (when (derived-mode-p 'elgantt-mode)
;; (let* ((inhibit-modification-hooks t) (let* ((inhibit-modification-hooks t)
;; (header-line-1 (save-excursion (header-line-1 (save-excursion
;; (goto-char (point-min)) (goto-char (point-min))
;; (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
;; (col-indices '()) (col-indices '())
;; (search-pos 0)) (search-pos 0))
;; (save-excursion (save-excursion
;; (save-restriction (save-restriction
;; (widen) (widen)
;; ;; 1. Clear ALL weekend overlays in the entire buffer ;; 1. Clear ALL weekend overlays in the entire buffer
;; (remove-overlays (point-min) (point-max) 'gortium-weekend t) (remove-overlays (point-min) (point-max) 'gortium-weekend t)
;; ;; 2. Parse header once to find column indexes (Fast) ;; 2. Parse header once to find column indexes (Fast)
;; (while (string-match "|[[:space:]]*\\([[:alpha:]]+\\)[[:space:]]+\\([0-9]\\{4\\}\\)" header-line-1 search-pos) (while (string-match "|[[:space:]]*\\([[:alpha:]]+\\)[[:space:]]+\\([0-9]\\{4\\}\\)" header-line-1 search-pos)
;; (let* ((month-start-col (match-beginning 0)) (let* ((month-start-col (match-beginning 0))
;; (month-name (match-string 1 header-line-1)) (month-name (match-string 1 header-line-1))
;; (year (string-to-number (match-string 2 header-line-1))) (year (string-to-number (match-string 2 header-line-1)))
;; (month-num (gortium/internal--month-to-num month-name)) (month-num (gortium/internal--month-to-num month-name))
;; (next-pipe (string-match "|" header-line-1 (1+ month-start-col))) (next-pipe (string-match "|" header-line-1 (1+ month-start-col)))
;; (month-width (if next-pipe (- next-pipe month-start-col 1) 31))) (month-width (if next-pipe (- next-pipe month-start-col 1) 31)))
;; (dotimes (d month-width) (dotimes (d month-width)
;; (let* ((day (1+ d)) (let* ((day (1+ d))
;; (time (condition-case nil (encode-time 0 0 12 day month-num year) (error nil)))) (time (condition-case nil (encode-time 0 0 12 day month-num year) (error nil))))
;; (when time (when time
;; (let ((dow (nth 6 (decode-time time))) (let ((dow (nth 6 (decode-time time)))
;; (actual-col (+ month-start-col 1 d))) (actual-col (+ month-start-col 1 d)))
;; (when (member dow '(0 6)) (when (member dow '(0 6))
;; (push actual-col col-indices)))))) (push actual-col col-indices))))))
;; (setq search-pos (or next-pipe (length header-line-1))))) (setq search-pos (or next-pipe (length header-line-1)))))
;; ;; 3. Apply to the WHOLE buffer line by line ;; 3. Apply to the WHOLE buffer line by line
;; (unless (null col-indices) (unless (null col-indices)
;; (goto-char (point-min)) (goto-char (point-min))
;; (forward-line 2) ;; Skip headers (forward-line 2) ;; Skip headers
;; (while (not (eobp)) (while (not (eobp))
;; (let ((line-end (line-end-position))) (let ((line-end (line-end-position)))
;; (dolist (col col-indices) (dolist (col col-indices)
;; (move-to-column col) (move-to-column col)
;; (let ((p (point))) (let ((p (point)))
;; ;; Ensure we are still on the same line and at the correct column ;; Ensure we are still on the same line and at the correct column
;; (when (and (< p line-end) (= (current-column) col)) (when (and (< p line-end) (= (current-column) col))
;; (let ((ov (make-overlay p (1+ p)))) (let ((ov (make-overlay p (1+ p))))
;; (overlay-put ov 'face 'gortium/elgantt-weekend-face) (overlay-put ov 'face 'gortium/elgantt-weekend-face)
;; (overlay-put ov 'gortium-weekend t) (overlay-put ov 'gortium-weekend t)
;; (overlay-put ov 'priority 100) (overlay-put ov 'priority 100)
;; (overlay-put ov 'evaporate t)))))) (overlay-put ov 'evaporate t))))))
;; (forward-line 1))))) (forward-line 1)))))
;; (message "Weekend guides rendered for the whole buffer.")))) (message "Weekend guides rendered for the whole buffer."))))
;; ;; Run it only once when the buffer is loaded ;; Run it only once when the buffer is loaded
;; (add-hook 'elgantt-mode-hook #'gortium/elgantt-draw-weekend-guides) (add-hook 'elgantt-mode-hook #'gortium/elgantt-draw-weekend-guides)
;; (use-package! elgantt (use-package! elgantt
;; :commands (elgantt-open elgantt-open-current-org-file) :commands (elgantt-open elgantt-open-current-org-file)
;; :config :config
;; ;; --- 1. Environment & UI --- ;; --- 1. Environment & UI ---
;; (add-hook 'elgantt-mode-hook (add-hook 'elgantt-mode-hook
;; (lambda () (lambda ()
;; (setq-local org-phscroll-mode nil) (setq-local org-phscroll-mode nil)
;; (setq-local image-roll-mode nil) (setq-local image-roll-mode nil)
;; (setq truncate-lines t))) (setq truncate-lines t)))
;; (setq elgantt-start-date "2026-01-01") (setq elgantt-start-date "2026-01-01")
;; (setq elgantt-header-column-offset 40 (setq elgantt-header-column-offset 40
;; elgantt-header-type 'root elgantt-header-type 'root
;; elgantt-show-header-depth t elgantt-show-header-depth t
;; elgantt-insert-blank-line-between-top-level-header t elgantt-insert-blank-line-between-top-level-header t
;; elgantt-startup-folded nil elgantt-startup-folded nil
;; elgantt-draw-overarching-headers nil elgantt-draw-overarching-headers nil
;; elgantt-scroll-to-current-month-at-startup 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 ;; --- Rule 1: Active Timestamp Range ---
;; :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR"))) (elgantt-create-display-rule draw-active-timestamp-range
;; (split-string colors " ")))) :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
;; (range-dates . ((save-excursion (split-string colors " "))))
;; (org-back-to-heading t) (range-dates . ((save-excursion
;; (let ((limit (save-excursion (outline-next-heading) (point)))) (org-back-to-heading t)
;; (when (re-search-forward "<\\([^>]+\\)>--<\\([^>]+\\)>" limit t) (let ((limit (save-excursion (outline-next-heading) (point))))
;; (list (match-string 1) (match-string 2)))))))) (when (re-search-forward "<\\([^>]+\\)>--<\\([^>]+\\)>" limit t)
;; :args (elgantt-org-id) (list (match-string 1) (match-string 2))))))))
;; :body ((when (and elgantt-org-id range-dates) :args (elgantt-org-id)
;; (let* ((colors (or override-color '("#fabd2f" "#fe8019"))) :body ((when (and elgantt-org-id range-dates)
;; (s-str (substring (car range-dates) 0 10)) (let* ((colors (or override-color '("#fabd2f" "#fe8019")))
;; (e-str (substring (cadr range-dates) 0 10)) (s-str (substring (car range-dates) 0 10))
;; (p1 (save-excursion (when (elgantt--goto-date s-str) (point)))) (e-str (substring (cadr range-dates) 0 10))
;; (p2 (save-excursion (when (elgantt--goto-date e-str) (point))))) (p1 (save-excursion (when (elgantt--goto-date s-str) (point))))
;; (when (and (numberp p1) (numberp p2)) (p2 (save-excursion (when (elgantt--goto-date e-str) (point)))))
;; (elgantt--draw-gradient (when (and (numberp p1) (numberp p2))
;; (car colors) (cadr colors) (elgantt--draw-gradient
;; (truncate p1) (truncate p2) nil ;; <-- FIX: Removed (1+ ...) to stop overshoot (car colors) (cadr colors)
;; `(priority ,(setq elgantt-user-set-color-priority-counter (truncate p1) (truncate p2) nil
;; (1- elgantt-user-set-color-priority-counter)) `(priority ,(setq elgantt-user-set-color-priority-counter
;; :elgantt-user-overlay ,elgantt-org-id)))))))) (1- elgantt-user-set-color-priority-counter))
:elgantt-user-overlay ,elgantt-org-id)))))))
;; ;; --- 2. Effort Rule (With Weekend Extension) --- ;; --- Rule 2: Effort Rule ---
;; ;; (elgantt-create-display-rule draw-scheduled-to-effort-end (elgantt-create-display-rule draw-scheduled-to-effort-end
;; ;; :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR"))) :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
;; ;; (split-string colors " ")))) (split-string colors " "))))
;; ;; (elgantt-effort . ((org-entry-get (point) "EFFORT"))) (elgantt-effort . ((org-entry-get (point) "EFFORT")))
;; ;; (wknd-days . ((when-let ((val (org-entry-get (point) "WEEKEND_DAYS"))) (wknd-days . ((when-let ((val (org-entry-get (point) "WEEKEND_DAYS")))
;; ;; (string-to-number val))))) (string-to-number val)))))
;; ;; :args (elgantt-scheduled elgantt-effort elgantt-org-id) :args (elgantt-scheduled elgantt-effort elgantt-org-id)
;; ;; :body ((when (and elgantt-scheduled elgantt-effort) :body ((when (and elgantt-scheduled elgantt-effort)
;; ;; (let* ((start-ts (ts-parse elgantt-scheduled)) (let* ((start-ts (ts-parse elgantt-scheduled))
;; ;; (raw-mins (org-duration-to-minutes elgantt-effort)) (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)))
;; ;; (total-days (+ (ceiling (/ (float raw-mins) 1440.0)) (or wknd-days 0))) (p1 (save-excursion
;; ;; (p1 (save-excursion (elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
;; ;; (elgantt--goto-date (ts-format "%Y-%m-%d" start-ts)) (point)))
;; ;; (point))) (colors (or override-color '("#8ec07c" "#458588"))))
;; ;; (colors (or override-color '("#8ec07c" "#458588")))) (when (numberp p1)
;; ;; (when (numberp p1) (if (<= total-days 1)
;; ;; (if (<= total-days 1) (elgantt--create-overlay (truncate p1) (1+ (truncate p1))
;; ;; (elgantt--create-overlay (truncate p1) (1+ (truncate p1)) `(face (:background ,(car colors))
;; ;; `(face (:background ,(car colors)) priority ,(setq elgantt-user-set-color-priority-counter
;; ;; priority ,(setq elgantt-user-set-color-priority-counter (1- elgantt-user-set-color-priority-counter))
;; ;; (1- elgantt-user-set-color-priority-counter)) :elgantt-user-overlay ,elgantt-org-id))
;; ;; :elgantt-user-overlay ,elgantt-org-id)) (let* ((end-ts (ts-adjust 'day (- total-days 2) start-ts))
;; ;; ;; FIX 1: compute p2 by date (handles "|" separators) (p2 (save-excursion
;; ;; ;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot (elgantt--goto-date (ts-format "%Y-%m-%d" end-ts))
;; ;; (let* ((end-ts (ts-adjust 'day (- total-days 2) start-ts)) (point))))
;; ;; (p2 (save-excursion (when (numberp p2)
;; ;; (elgantt--goto-date (ts-format "%Y-%m-%d" end-ts)) (elgantt--draw-gradient
;; ;; (point)))) (car colors) (cadr colors)
;; ;; (when (numberp p2) (truncate p1) (1+ (truncate p2)) nil
;; ;; (elgantt--draw-gradient `(priority ,(setq elgantt-user-set-color-priority-counter
;; ;; (car colors) (cadr colors) (1- elgantt-user-set-color-priority-counter))
;; ;; (truncate p1) (1+ (truncate p2)) nil :elgantt-user-overlay ,elgantt-org-id)))))))))))
;; ;; `(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 ;; --- Rule 3: Blocker Lines ---
;; :parser ((blocker-raw . ((org-entry-get (point) "BLOCKER")))) (elgantt-create-display-rule draw-blocker-lines
;; :args (elgantt-org-id elgantt-scheduled) :parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
;; :body ((when (and elgantt-org-id blocker-raw (not (string-empty-p blocker-raw))) :args (elgantt-org-id elgantt-scheduled)
;; ;; 1. GET DESTINATION (Start of current task) :body ((when (and elgantt-org-id blocker-raw (not (string-empty-p blocker-raw)))
;; ;; We use the built-in elgantt-scheduled arg if available, it's faster and safer. (let* ((p-dest (save-excursion
;; (let* ((p-dest (save-excursion (let ((d-start (or (when (stringp elgantt-scheduled) (substring elgantt-scheduled 0 10))
;; (let ((d-start (or (when (stringp elgantt-scheduled) (substring elgantt-scheduled 0 10)) (elgantt-with-point-at-orig-entry nil
;; (elgantt-with-point-at-orig-entry nil (save-excursion
;; (save-excursion (org-back-to-heading t)
;; (org-back-to-heading t) (when (re-search-forward "<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" (line-end-position) t)
;; (when (re-search-forward "<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" (line-end-position) t) (match-string 1)))))))
;; (match-string 1))))))) (when (and d-start (elgantt--goto-date d-start)) (point))))))
;; (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) (defun elgantt-open-current-org-file ()
;; (let ((ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw)) (interactive)
;; (id-list (split-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw) "[ ,]+" t))) (if-let ((file (buffer-file-name)))
;; (dolist (blocker-id id-list) (progn
;; (save-excursion (setq elgantt-agenda-files (list file))
;; (when (elgantt--goto-id blocker-id) (elgantt--reset-org-ql-cache)
;; (let ((d-end-str nil) (elgantt-open))
;; (row-start (line-beginning-position)) (message "No file!")))
;; (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!")))
#+end_src #+end_src
#+RESULTS: #+RESULTS:
@@ -2351,36 +2343,38 @@ Returns nil if no range found (safe, non-blocking)."
(when (and (string-match "ids(\\(.*?\\))" clean) (when (and (string-match "ids(\\(.*?\\))" clean)
(not (string-empty-p (s-trim (match-string 1 clean))))) (not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) (let* ((clean-id (replace-regexp-in-string "[\"']\\|id: " "" tid))
(pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos) (pos (org-id-find clean-id t))
(computed-end (gethash clean-id task-end-map)) (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 (cond
(computed-end computed-end) (computed-end computed-end) ;; Use what we just calculated in this session
(pos (let ((m (if (markerp pos) pos
;; Use a temporary buffer context to check properties if pos is a list (set-marker (make-marker) (cdr pos)
((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) (find-file-noselect (car pos))))))
(with-current-buffer (marker-buffer m) (with-current-buffer (marker-buffer m)
(org-with-point-at m (org-with-point-at m
(cond (cond
((org-entry-get nil "CLOSED") ;; Priority 1: Use actual CLOSED timestamp if DONE
(org-time-string-to-time (org-entry-get nil "CLOSED"))) ((org-entry-get nil "CLOSED")
((string-equal "t" (org-entry-get nil "FIXED")) (org-time-string-to-time (org-entry-get nil "CLOSED")))
(gortium/org--get-range-end m)) ;; Priority 2: Use range end if FIXED
(t nil)))))) ((string-equal "t" (org-entry-get nil "FIXED"))
;; This captures the result of the let/with-current-buffer block (gortium/org--get-range-end m))
(let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) (t nil))))))
(with-current-buffer (marker-buffer m) (t nil)))
(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))))
;; 2. Update the "Latest" tracker
(if blocker-end (if blocker-end
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end)) (when (or (null latest-time) (time-less-p latest-time blocker-end))
blocker-end latest-time)) (setq latest-time blocker-end))
;; If ANY blocker is not resolved/found, the whole task is not ready
(setq all-resolved nil))))) (setq all-resolved nil)))))
;; Only return a time if EVERY ID in the list was successfully resolved
(when all-resolved latest-time))) (when all-resolved latest-time)))
(defun gortium/internal--update-properties (pos start wknd id end task-end-map) (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) (unless (eobp)
(insert "\n"))))) (insert "\n")))))
(message "[DEBUG] exit update-properties") (message "[DEBUG] exit update-properties")
(puthash id end task-end-map))) (puthash id end task-end-map)))
;; ------------------------------------------------------------ ;; ------------------------------------------------------------
;; Helper: Detect circular dependencies ;; 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 (with-current-buffer buf
(org-element-with-disabled-cache (org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0)) (let* ((off-days (if (stringp offset) (string-to-number offset) 0))
;; CRITICAL: For fixed tasks, base-start is THEIR start. (base-start (cond
;; For dependent tasks, base-start is the blocker's END. ;; 1. If FIXED, use its own defined start
(base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time))) (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
(t (or blocker-end (or 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 (final-start (if is-fixed base-start
(gortium/internal--snap-to-working-hours (gortium/internal--snap-to-working-hours

View File

@@ -400,227 +400,6 @@
;; Enable plantuml-mode for PlantUML files ;; Enable plantuml-mode for PlantUML files
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode)) (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/") (setq org-roam-directory (file-truename "~/ExoKortex/")
org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db") org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db")
org-attach-id-dir "assets/" org-attach-id-dir "assets/"
@@ -1697,36 +1476,38 @@ Returns nil if no range found (safe, non-blocking)."
(when (and (string-match "ids(\\(.*?\\))" clean) (when (and (string-match "ids(\\(.*?\\))" clean)
(not (string-empty-p (s-trim (match-string 1 clean))))) (not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) (let* ((clean-id (replace-regexp-in-string "[\"']\\|id: " "" tid))
(pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos) (pos (org-id-find clean-id t))
(computed-end (gethash clean-id task-end-map)) (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 (cond
(computed-end computed-end) (computed-end computed-end) ;; Use what we just calculated in this session
(pos (let ((m (if (markerp pos) pos
;; Use a temporary buffer context to check properties if pos is a list (set-marker (make-marker) (cdr pos)
((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) (find-file-noselect (car pos))))))
(with-current-buffer (marker-buffer m) (with-current-buffer (marker-buffer m)
(org-with-point-at m (org-with-point-at m
(cond (cond
((org-entry-get nil "CLOSED") ;; Priority 1: Use actual CLOSED timestamp if DONE
(org-time-string-to-time (org-entry-get nil "CLOSED"))) ((org-entry-get nil "CLOSED")
((string-equal "t" (org-entry-get nil "FIXED")) (org-time-string-to-time (org-entry-get nil "CLOSED")))
(gortium/org--get-range-end m)) ;; Priority 2: Use range end if FIXED
(t nil)))))) ((string-equal "t" (org-entry-get nil "FIXED"))
;; This captures the result of the let/with-current-buffer block (gortium/org--get-range-end m))
(let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) (t nil))))))
(with-current-buffer (marker-buffer m) (t nil)))
(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))))
;; 2. Update the "Latest" tracker
(if blocker-end (if blocker-end
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end)) (when (or (null latest-time) (time-less-p latest-time blocker-end))
blocker-end latest-time)) (setq latest-time blocker-end))
;; If ANY blocker is not resolved/found, the whole task is not ready
(setq all-resolved nil))))) (setq all-resolved nil)))))
;; Only return a time if EVERY ID in the list was successfully resolved
(when all-resolved latest-time))) (when all-resolved latest-time)))
(defun gortium/internal--update-properties (pos start wknd id end task-end-map) (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) (unless (eobp)
(insert "\n"))))) (insert "\n")))))
(message "[DEBUG] exit update-properties") (message "[DEBUG] exit update-properties")
(puthash id end task-end-map))) (puthash id end task-end-map)))
;; ------------------------------------------------------------ ;; ------------------------------------------------------------
;; Helper: Detect circular dependencies ;; 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 (with-current-buffer buf
(org-element-with-disabled-cache (org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0)) (let* ((off-days (if (stringp offset) (string-to-number offset) 0))
;; CRITICAL: For fixed tasks, base-start is THEIR start. (base-start (cond
;; For dependent tasks, base-start is the blocker's END. ;; 1. If FIXED, use its own defined start
(base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time))) (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
(t (or blocker-end (or 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 (final-start (if is-fixed base-start
(gortium/internal--snap-to-working-hours (gortium/internal--snap-to-working-hours