Committing before i loose it again!
This commit is contained in:
@@ -464,16 +464,18 @@ change ~org-directory~. It must be set before org loads!
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+perso")))
|
||||
)
|
||||
("wP" "THE PLAN"
|
||||
("wP" "Installation Bombardier"
|
||||
((agenda ""
|
||||
((org-agenda-span 60)
|
||||
(org-agenda-start-day nil)
|
||||
(org-agenda-overriding-header "📅 THE PLAN")
|
||||
(org-agenda-prefix-format " %?-12t%-12s")
|
||||
)
|
||||
)
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+work")))
|
||||
(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 " %-12s ") ;; fixed-width TODO
|
||||
(org-agenda-tags-column -100) ;; right-align tags
|
||||
(org-agenda-time-grid nil)
|
||||
)))
|
||||
|
||||
((org-agenda-tag-filter-preset '("+BA_ON_SITE")))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -570,237 +572,226 @@ fc --> UC3
|
||||
|
||||
* Elgantt
|
||||
#+begin_src emacs-lisp
|
||||
(require 'cl-lib)
|
||||
(require 'dash)
|
||||
(unless (fboundp 'first) (defalias 'first #'car))
|
||||
;; (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)
|
||||
;; ;; 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 t)
|
||||
;; (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)
|
||||
|
||||
;; --- 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-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))))))))
|
||||
|
||||
;; --- 3. Progress Bar ---
|
||||
(elgantt-create-display-rule pages-read-progress
|
||||
:parser ((total-pages . ((--when-let (org-entry-get (point) "TOTAL_PAGES") (string-to-number it))))
|
||||
(pages-read . ((--when-let (org-entry-get (point) "PAGES_READ") (string-to-number it)))))
|
||||
:args (elgantt-deadline elgantt-scheduled)
|
||||
:body ((when (and elgantt-deadline elgantt-scheduled total-pages pages-read)
|
||||
(let* ((start (save-excursion (elgantt--goto-date elgantt-scheduled) (point)))
|
||||
(end (save-excursion (elgantt--goto-date elgantt-deadline) (point)))
|
||||
(percent (/ (float pages-read) (float total-pages))))
|
||||
(when (and (numberp start) (numberp end))
|
||||
(elgantt--draw-progress-bar "#98be65" "#ff6c6b"
|
||||
(truncate start) (truncate end) percent))))))
|
||||
;; ;; --- 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))))))))))
|
||||
|
||||
;; --- 4. Blocker Interaction (Smart Append) ---
|
||||
(require 'elgantt-interaction)
|
||||
(elgantt--selection-rule :name mark-blocker
|
||||
:selection-number 2
|
||||
:selection-messages ((1 . "Select the BLOCKING task (Cause)")
|
||||
(2 . "Select the BLOCKED task (Effect)"))
|
||||
:execution-functions
|
||||
((1 . ((elgantt-with-point-at-orig-entry nil (org-id-get-create))))
|
||||
(2 . ((let* ((new-id return-val)
|
||||
(current (elgantt-with-point-at-orig-entry nil (org-entry-get (point) "BLOCKER"))))
|
||||
(elgantt-with-point-at-orig-entry nil
|
||||
(if (and current (string-match "ids(\\(.*?\\))" current))
|
||||
(let ((existing (match-string 1 current)))
|
||||
(org-set-property "BLOCKER" (format "ids(%s %s)" existing new-id)))
|
||||
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
||||
(message "Added blocker: %s" new-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))))))
|
||||
|
||||
;; --- 5. Blocker Lines (Surgical Alignment Fix) ---
|
||||
(elgantt-create-display-rule draw-blocker-lines
|
||||
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
||||
:args (elgantt-org-id)
|
||||
:body ((when (and blocker-raw (not (string-empty-p blocker-raw)))
|
||||
(let* ((p-blocked (point))
|
||||
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
||||
(match-string 1 blocker-raw)
|
||||
blocker-raw))
|
||||
(id-list (split-string ids-string "[ ,]+" t)))
|
||||
(dolist (blocker-id id-list)
|
||||
(save-excursion
|
||||
(when (elgantt--goto-id blocker-id)
|
||||
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
||||
(list (org-entry-get (point) "SCHEDULED")
|
||||
(org-entry-get (point) "EFFORT")
|
||||
(org-entry-get (point) "WEEKEND_DAYS"))))
|
||||
(b-sched (nth 0 blocker-data))
|
||||
(b-effort (nth 1 blocker-data))
|
||||
(b-wknd (when (nth 2 blocker-data) (string-to-number (nth 2 blocker-data)))))
|
||||
(when (and b-sched b-effort)
|
||||
(let* ((start-ts (ts-parse b-sched))
|
||||
(raw-mins (org-duration-to-minutes b-effort))
|
||||
;; Visual length must match the Effort Rule exactly
|
||||
(total-days (+ (ceiling (/ (float raw-mins) 1440.0)) (or b-wknd 0)))
|
||||
(p-start (save-excursion
|
||||
(elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
|
||||
(point))))
|
||||
(when (and (numberp p-start) (numberp p-blocked))
|
||||
;; Point to the LAST DAY of the task bar
|
||||
(let* ((end-date-ts (ts-adjust 'day (max 0 (1- total-days)) start-ts))
|
||||
(p-line-start (save-excursion
|
||||
(elgantt--goto-date (ts-format "%Y-%m-%d" end-date-ts))
|
||||
(point))))
|
||||
(when (numberp p-line-start)
|
||||
;; DRAW: From p-line-start to p-blocked
|
||||
;; Note: Removed the (1+) to pull the line back by one day
|
||||
(elgantt--draw-line (truncate p-line-start)
|
||||
(truncate p-blocked)
|
||||
"#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))))))))))
|
||||
|
||||
;; --- 6. Hashtag Navigation ---
|
||||
(elgantt-create-action follow-hashtag-link-forward
|
||||
:args (elgantt-alltags) :binding "C-M-f"
|
||||
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it) elgantt-alltags))
|
||||
(match (elgantt--next-match :elgantt-alltags hashtag)))
|
||||
(goto-char (car match)))))
|
||||
;; ;; 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")))))))))))))))
|
||||
;; )
|
||||
|
||||
(elgantt-create-action follow-hashtag-link-backward
|
||||
:args (elgantt-alltags) :binding "C-M-b"
|
||||
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it) elgantt-alltags))
|
||||
(match (elgantt--previous-match :elgantt-alltags hashtag)))
|
||||
(goto-char (car match)))))
|
||||
)
|
||||
|
||||
(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:
|
||||
@@ -2087,6 +2078,9 @@ www.tdnde.com \\\\
|
||||
#+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
|
||||
@@ -2210,7 +2204,7 @@ If FILE is nil, refile in the current file."
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
;;; ============================================================
|
||||
;;; GORTIUM — Org chain scheduler (timestamp-based, no duplicates)
|
||||
;;; GORTIUM — Org chain scheduler (Safe & Optimized with Debugging)
|
||||
;;; ============================================================
|
||||
|
||||
(require 'org)
|
||||
@@ -2254,9 +2248,30 @@ If FILE is nil, refile in the current file."
|
||||
(nthcdr 3 (decode-time next)))))))
|
||||
(t t1))))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Calculate task span using EFFORT (hour-grained)
|
||||
;; ------------------------------------------------------------
|
||||
(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))
|
||||
@@ -2268,8 +2283,11 @@ If FILE is nil, refile in the current file."
|
||||
(cursor start-time)
|
||||
(wknd-count 0)
|
||||
(day-start 8)
|
||||
(day-end 16))
|
||||
(while (> total-work-mins 0)
|
||||
(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))
|
||||
@@ -2293,183 +2311,242 @@ If FILE is nil, refile in the current file."
|
||||
(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 current-pos task-end-map)
|
||||
"Return the latest end time among the dependencies listed in BLOCKER-STR."
|
||||
(defun gortium/internal--get-blocker-end (blocker-str task-end-map)
|
||||
"Return latest end time only if blockers are DONE or have been recalculated."
|
||||
(let ((clean (s-trim (format "%s" blocker-str)))
|
||||
(latest-time nil))
|
||||
(cond
|
||||
((string-match-p "previous-sibling" clean)
|
||||
(save-excursion
|
||||
(goto-char current-pos)
|
||||
(let ((found nil))
|
||||
(while (and (not found) (org-get-last-sibling))
|
||||
(let* ((sid (org-id-get))
|
||||
(end (when sid (gethash sid task-end-map))))
|
||||
(when end
|
||||
(setq latest-time end
|
||||
found t)))))))
|
||||
((string-match-p "parent" clean)
|
||||
(save-excursion
|
||||
(goto-char current-pos)
|
||||
(when (org-up-heading-safe)
|
||||
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
||||
((string-match "ids(\\(.*?\\))" clean)
|
||||
(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) "[ ,]+" t))
|
||||
(let ((tend (gethash (replace-regexp-in-string "[\"']\\|id:" "" tid) task-end-map)))
|
||||
(when (and tend
|
||||
(or (null latest-time)
|
||||
(time-less-p latest-time tend)))
|
||||
(setq latest-time tend))))))
|
||||
latest-time))
|
||||
(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
|
||||
(cond
|
||||
;; 1) Use the new time we just calculated in this run (Priority!)
|
||||
(computed-end computed-end)
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Get earliest timestamp in entry
|
||||
;; ------------------------------------------------------------
|
||||
(defun gortium/org--get-anchor-time ()
|
||||
"Return the earliest timestamp in current entry, or nil."
|
||||
(save-excursion
|
||||
(org-back-to-heading t)
|
||||
(let ((limit (save-excursion (outline-next-heading) (point)))
|
||||
(best nil))
|
||||
(while (re-search-forward org-ts-regexp-both limit t 1)
|
||||
(let ((ts (org-time-string-to-time (match-string 0))))
|
||||
(when (or (null best) (time-less-p ts best))
|
||||
(setq best ts))))
|
||||
best)))
|
||||
;; 2) If it's DONE, use the CLOSED timestamp
|
||||
((and pos (org-entry-get pos "CLOSED"))
|
||||
(org-time-string-to-time (org-entry-get pos "CLOSED")))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Write timestamp range (NO SCHEDULED)
|
||||
;; ------------------------------------------------------------
|
||||
(defun gortium/internal--update-properties (pos start wknd id end task-end-map &optional fixed original-start)
|
||||
"Update WEEKEND_DAYS and write the time range at POS.
|
||||
For fixed tasks, ORIGINAL-START is preserved. OLD SCHEDULED/range lines are removed.
|
||||
The range is inserted directly below the heading, before any drawers."
|
||||
;; 3) If it's FIXED, use the existing range/scheduled time
|
||||
((and pos (string-equal "t" (org-entry-get pos "FIXED")))
|
||||
(or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback
|
||||
(org-get-scheduled-time pos)))
|
||||
|
||||
;; Otherwise: We MUST wait for this blocker to be recalculated
|
||||
(t nil))))
|
||||
|
||||
(if blocker-end
|
||||
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end))
|
||||
blocker-end latest-time))
|
||||
(setq all-resolved nil)))))
|
||||
(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-show-entry)
|
||||
|
||||
;; Update WEEKEND_DAYS property
|
||||
(if (and wknd (> wknd 0))
|
||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string wknd))
|
||||
(org-entry-delete (point) "WEEKEND_DAYS"))
|
||||
|
||||
;; Remove old ranges or SCHEDULED lines
|
||||
(save-excursion
|
||||
(org-back-to-heading t)
|
||||
(let ((limit (save-excursion (outline-next-heading) (point))))
|
||||
(forward-line 1)
|
||||
(while (< (point) limit)
|
||||
(cond
|
||||
((looking-at "^[ \t]*SCHEDULED:") (delete-region (point-at-bol) (1+ (point-at-eol))))
|
||||
((looking-at "^[ \t]*<.*>--<.*>") (delete-region (point-at-bol) (1+ (point-at-eol))))
|
||||
((looking-at "^\\*+") (goto-char limit))
|
||||
((looking-at "^[ \t]*:") (goto-char (line-beginning-position))
|
||||
(setq limit (point)))
|
||||
(t (forward-line 1))))))
|
||||
(let* ((subtree-start (point))
|
||||
(subtree-end (save-excursion (org-end-of-subtree t) (point))))
|
||||
|
||||
;; Determine insertion point: after heading, before drawers
|
||||
(let ((insert-point (save-excursion
|
||||
(org-back-to-heading t)
|
||||
(forward-line 1)
|
||||
(point)))
|
||||
(range-start (if fixed original-start start))) ;; preserve start if fixed
|
||||
;; Insert range
|
||||
(goto-char insert-point)
|
||||
(insert (format "%s--%s"
|
||||
(format-time-string "<%Y-%m-%d %a %H:%M>" range-start)
|
||||
(format-time-string "<%Y-%m-%d %a %H:%M>" end))
|
||||
"\n"))
|
||||
(save-restriction
|
||||
(narrow-to-region subtree-start subtree-end)
|
||||
|
||||
;; Update task-end-map
|
||||
;; --- 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)))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; MAIN FUNCTION
|
||||
;; 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 ()
|
||||
"Optimized scheduler: Ignores stale buffer ranges to ensure correct dependency flow."
|
||||
(interactive)
|
||||
(save-excursion
|
||||
(message "--- Starting Global Gantt Scheduler ---")
|
||||
(let ((all-tasks '())
|
||||
(message "=== Starting Gortium Scheduler ===")
|
||||
|
||||
;; 1. Global deactivations to prevent the "Parser Error"
|
||||
(let ((org-element-use-cache nil)
|
||||
(all-tasks '())
|
||||
(task-end-times (make-hash-table :test 'equal))
|
||||
(task-end-for-blockers (make-hash-table :test 'equal)))
|
||||
;; COLLECTION
|
||||
(start-time (current-time)))
|
||||
|
||||
;; 2. COLLECTION
|
||||
(org-map-entries
|
||||
(lambda ()
|
||||
(let ((state (org-get-todo-state)))
|
||||
(when state
|
||||
(when (org-get-todo-state)
|
||||
(let* ((pos (point-marker))
|
||||
(id (org-id-get-create))
|
||||
(effort (org-entry-get (point) "EFFORT"))
|
||||
(blocker (org-entry-get (point) "BLOCKER"))
|
||||
(fixed (org-entry-get (point) "FIXED"))
|
||||
(anchor (or (org-get-scheduled-time (point))
|
||||
(gortium/org--get-anchor-time)))
|
||||
(closed (org-entry-get (point) "CLOSED"))
|
||||
(offset (org-entry-get (point) "OFFSET_DAYS")))
|
||||
(when (or effort blocker fixed (string= state "DONE"))
|
||||
(push (list pos id effort blocker fixed anchor offset state closed)
|
||||
all-tasks))))))
|
||||
(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))
|
||||
|
||||
;; PASS 1: DONE & FIXED
|
||||
(dolist (task all-tasks)
|
||||
(pcase-let ((`(,pos ,id ,effort ,_ ,fixed ,anchor ,_ ,state ,closed) task))
|
||||
(cond
|
||||
;; DONE: compute range like normal, CLOSED is used for blockers
|
||||
((string= state "DONE")
|
||||
(let* ((start (or anchor (current-time)))
|
||||
(span (gortium/internal--calculate-task-span start effort)))
|
||||
(gortium/internal--update-properties pos (car span) (cadr span) id (car span)
|
||||
task-end-times))
|
||||
;; Store CLOSED for dependencies
|
||||
(when closed
|
||||
(puthash id (org-time-string-to-time closed) task-end-for-blockers)))
|
||||
;; FIXED tasks
|
||||
((and fixed anchor)
|
||||
(let* ((span (gortium/internal--calculate-task-span anchor effort)))
|
||||
(gortium/internal--update-properties pos anchor (cadr span) id (car span)
|
||||
task-end-times)))
|
||||
;; nothing else here
|
||||
)))
|
||||
|
||||
;; PASS 2: CHAINS
|
||||
(let ((remaining (cl-remove-if (lambda (t)
|
||||
(gethash (nth 1 t) task-end-times))
|
||||
all-tasks))
|
||||
(limit (* 5 (length all-tasks)))
|
||||
;; 3. THE LOOP
|
||||
(let* ((remaining all-tasks)
|
||||
(limit (* 20 (length remaining)))
|
||||
(iter 0))
|
||||
(while (and remaining (< iter limit))
|
||||
(cl-incf iter)
|
||||
(let ((done '()))
|
||||
(dolist (task remaining)
|
||||
(pcase-let ((`(,pos ,id ,effort ,blocker ,_ ,_ ,offset ,state ,closed) task))
|
||||
;; Determine dependency end
|
||||
(let* ((dep (or (gortium/internal--get-blocker-end blocker pos
|
||||
task-end-times)
|
||||
(and (string= state "DONE")
|
||||
(gethash id task-end-for-blockers)))))
|
||||
(when dep
|
||||
(let* ((off (if offset (string-to-number offset) 0))
|
||||
(start (gortium/internal--snap-to-working-hours
|
||||
(time-add dep (days-to-time off))))
|
||||
(span (gortium/internal--calculate-task-span start effort)))
|
||||
;; FIXED tasks: keep start
|
||||
(if (and (not (string= state "DONE")) (org-entry-get pos "FIXED"))
|
||||
(gortium/internal--update-properties pos anchor (cadr span) id (car span)
|
||||
task-end-times)
|
||||
(gortium/internal--update-properties pos (car span) (cadr span) id (car span)
|
||||
task-end-times))
|
||||
(push task done))))))
|
||||
(setq remaining (cl-set-difference remaining done)))))
|
||||
|
||||
(message "--- Scheduler Finished ---"))))
|
||||
(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)))))
|
||||
;; A task is ready if it's FIXED or all blockers are in the task-end-times map
|
||||
(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 (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
|
||||
(t (or blocker-end 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))))
|
||||
|
||||
(org-element-cache-reset 'all)
|
||||
(message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter))))
|
||||
|
||||
|
||||
;; ---------------------------------------------
|
||||
(defun gortium/org-ensure-task-properties ()
|
||||
@@ -2482,7 +2559,7 @@ and ensure the standard property drawer exists without overwriting existing data
|
||||
;; List of properties to ensure exist
|
||||
(props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS"
|
||||
"ASSIGNEE" "RESOURCES" "CATEGORY"
|
||||
"DIMENTIONS" "WEIGHT")))
|
||||
"DIMENTIONS" "WEIGHT" "OFFSET_DAYS")))
|
||||
(org-map-entries
|
||||
(lambda ()
|
||||
;; This check returns true if the heading has ANY todo keyword
|
||||
|
||||
@@ -334,16 +334,18 @@
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+perso")))
|
||||
)
|
||||
("wP" "THE PLAN"
|
||||
("wP" "Installation Bombardier"
|
||||
((agenda ""
|
||||
((org-agenda-span 60)
|
||||
(org-agenda-start-day nil)
|
||||
(org-agenda-overriding-header "📅 THE PLAN")
|
||||
(org-agenda-prefix-format " %?-12t%-12s")
|
||||
)
|
||||
)
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+work")))
|
||||
(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 " %-12s ") ;; fixed-width TODO
|
||||
(org-agenda-tags-column -100) ;; right-align tags
|
||||
(org-agenda-time-grid nil)
|
||||
)))
|
||||
|
||||
((org-agenda-tag-filter-preset '("+BA_ON_SITE")))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -398,237 +400,226 @@
|
||||
;; 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))
|
||||
;; (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)
|
||||
;; ;; 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 t)
|
||||
;; (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)
|
||||
|
||||
;; --- 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-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))))))))
|
||||
|
||||
;; --- 3. Progress Bar ---
|
||||
(elgantt-create-display-rule pages-read-progress
|
||||
:parser ((total-pages . ((--when-let (org-entry-get (point) "TOTAL_PAGES") (string-to-number it))))
|
||||
(pages-read . ((--when-let (org-entry-get (point) "PAGES_READ") (string-to-number it)))))
|
||||
:args (elgantt-deadline elgantt-scheduled)
|
||||
:body ((when (and elgantt-deadline elgantt-scheduled total-pages pages-read)
|
||||
(let* ((start (save-excursion (elgantt--goto-date elgantt-scheduled) (point)))
|
||||
(end (save-excursion (elgantt--goto-date elgantt-deadline) (point)))
|
||||
(percent (/ (float pages-read) (float total-pages))))
|
||||
(when (and (numberp start) (numberp end))
|
||||
(elgantt--draw-progress-bar "#98be65" "#ff6c6b"
|
||||
(truncate start) (truncate end) percent))))))
|
||||
;; ;; --- 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))))))))))
|
||||
|
||||
;; --- 4. Blocker Interaction (Smart Append) ---
|
||||
(require 'elgantt-interaction)
|
||||
(elgantt--selection-rule :name mark-blocker
|
||||
:selection-number 2
|
||||
:selection-messages ((1 . "Select the BLOCKING task (Cause)")
|
||||
(2 . "Select the BLOCKED task (Effect)"))
|
||||
:execution-functions
|
||||
((1 . ((elgantt-with-point-at-orig-entry nil (org-id-get-create))))
|
||||
(2 . ((let* ((new-id return-val)
|
||||
(current (elgantt-with-point-at-orig-entry nil (org-entry-get (point) "BLOCKER"))))
|
||||
(elgantt-with-point-at-orig-entry nil
|
||||
(if (and current (string-match "ids(\\(.*?\\))" current))
|
||||
(let ((existing (match-string 1 current)))
|
||||
(org-set-property "BLOCKER" (format "ids(%s %s)" existing new-id)))
|
||||
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
||||
(message "Added blocker: %s" new-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))))))
|
||||
|
||||
;; --- 5. Blocker Lines (Surgical Alignment Fix) ---
|
||||
(elgantt-create-display-rule draw-blocker-lines
|
||||
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
||||
:args (elgantt-org-id)
|
||||
:body ((when (and blocker-raw (not (string-empty-p blocker-raw)))
|
||||
(let* ((p-blocked (point))
|
||||
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
||||
(match-string 1 blocker-raw)
|
||||
blocker-raw))
|
||||
(id-list (split-string ids-string "[ ,]+" t)))
|
||||
(dolist (blocker-id id-list)
|
||||
(save-excursion
|
||||
(when (elgantt--goto-id blocker-id)
|
||||
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
||||
(list (org-entry-get (point) "SCHEDULED")
|
||||
(org-entry-get (point) "EFFORT")
|
||||
(org-entry-get (point) "WEEKEND_DAYS"))))
|
||||
(b-sched (nth 0 blocker-data))
|
||||
(b-effort (nth 1 blocker-data))
|
||||
(b-wknd (when (nth 2 blocker-data) (string-to-number (nth 2 blocker-data)))))
|
||||
(when (and b-sched b-effort)
|
||||
(let* ((start-ts (ts-parse b-sched))
|
||||
(raw-mins (org-duration-to-minutes b-effort))
|
||||
;; Visual length must match the Effort Rule exactly
|
||||
(total-days (+ (ceiling (/ (float raw-mins) 1440.0)) (or b-wknd 0)))
|
||||
(p-start (save-excursion
|
||||
(elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
|
||||
(point))))
|
||||
(when (and (numberp p-start) (numberp p-blocked))
|
||||
;; Point to the LAST DAY of the task bar
|
||||
(let* ((end-date-ts (ts-adjust 'day (max 0 (1- total-days)) start-ts))
|
||||
(p-line-start (save-excursion
|
||||
(elgantt--goto-date (ts-format "%Y-%m-%d" end-date-ts))
|
||||
(point))))
|
||||
(when (numberp p-line-start)
|
||||
;; DRAW: From p-line-start to p-blocked
|
||||
;; Note: Removed the (1+) to pull the line back by one day
|
||||
(elgantt--draw-line (truncate p-line-start)
|
||||
(truncate p-blocked)
|
||||
"#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))))))))))
|
||||
|
||||
;; --- 6. Hashtag Navigation ---
|
||||
(elgantt-create-action follow-hashtag-link-forward
|
||||
:args (elgantt-alltags) :binding "C-M-f"
|
||||
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it) elgantt-alltags))
|
||||
(match (elgantt--next-match :elgantt-alltags hashtag)))
|
||||
(goto-char (car match)))))
|
||||
;; ;; 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")))))))))))))))
|
||||
;; )
|
||||
|
||||
(elgantt-create-action follow-hashtag-link-backward
|
||||
:args (elgantt-alltags) :binding "C-M-b"
|
||||
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it) elgantt-alltags))
|
||||
(match (elgantt--previous-match :elgantt-alltags hashtag)))
|
||||
(goto-char (car match)))))
|
||||
)
|
||||
|
||||
(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!")))
|
||||
|
||||
(setq org-roam-directory (file-truename "~/ExoKortex/")
|
||||
org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db")
|
||||
@@ -1559,7 +1550,7 @@ If FILE is nil, refile in the current file."
|
||||
(add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer)
|
||||
|
||||
;;; ============================================================
|
||||
;;; GORTIUM — Org chain scheduler (timestamp-based, no duplicates)
|
||||
;;; GORTIUM — Org chain scheduler (Safe & Optimized with Debugging)
|
||||
;;; ============================================================
|
||||
|
||||
(require 'org)
|
||||
@@ -1603,9 +1594,30 @@ If FILE is nil, refile in the current file."
|
||||
(nthcdr 3 (decode-time next)))))))
|
||||
(t t1))))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Calculate task span using EFFORT (hour-grained)
|
||||
;; ------------------------------------------------------------
|
||||
(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))
|
||||
@@ -1617,8 +1629,11 @@ If FILE is nil, refile in the current file."
|
||||
(cursor start-time)
|
||||
(wknd-count 0)
|
||||
(day-start 8)
|
||||
(day-end 16))
|
||||
(while (> total-work-mins 0)
|
||||
(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))
|
||||
@@ -1642,183 +1657,242 @@ If FILE is nil, refile in the current file."
|
||||
(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 current-pos task-end-map)
|
||||
"Return the latest end time among the dependencies listed in BLOCKER-STR."
|
||||
(defun gortium/internal--get-blocker-end (blocker-str task-end-map)
|
||||
"Return latest end time only if blockers are DONE or have been recalculated."
|
||||
(let ((clean (s-trim (format "%s" blocker-str)))
|
||||
(latest-time nil))
|
||||
(cond
|
||||
((string-match-p "previous-sibling" clean)
|
||||
(save-excursion
|
||||
(goto-char current-pos)
|
||||
(let ((found nil))
|
||||
(while (and (not found) (org-get-last-sibling))
|
||||
(let* ((sid (org-id-get))
|
||||
(end (when sid (gethash sid task-end-map))))
|
||||
(when end
|
||||
(setq latest-time end
|
||||
found t)))))))
|
||||
((string-match-p "parent" clean)
|
||||
(save-excursion
|
||||
(goto-char current-pos)
|
||||
(when (org-up-heading-safe)
|
||||
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
||||
((string-match "ids(\\(.*?\\))" clean)
|
||||
(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) "[ ,]+" t))
|
||||
(let ((tend (gethash (replace-regexp-in-string "[\"']\\|id:" "" tid) task-end-map)))
|
||||
(when (and tend
|
||||
(or (null latest-time)
|
||||
(time-less-p latest-time tend)))
|
||||
(setq latest-time tend))))))
|
||||
latest-time))
|
||||
(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
|
||||
(cond
|
||||
;; 1) Use the new time we just calculated in this run (Priority!)
|
||||
(computed-end computed-end)
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Get earliest timestamp in entry
|
||||
;; ------------------------------------------------------------
|
||||
(defun gortium/org--get-anchor-time ()
|
||||
"Return the earliest timestamp in current entry, or nil."
|
||||
(save-excursion
|
||||
(org-back-to-heading t)
|
||||
(let ((limit (save-excursion (outline-next-heading) (point)))
|
||||
(best nil))
|
||||
(while (re-search-forward org-ts-regexp-both limit t 1)
|
||||
(let ((ts (org-time-string-to-time (match-string 0))))
|
||||
(when (or (null best) (time-less-p ts best))
|
||||
(setq best ts))))
|
||||
best)))
|
||||
;; 2) If it's DONE, use the CLOSED timestamp
|
||||
((and pos (org-entry-get pos "CLOSED"))
|
||||
(org-time-string-to-time (org-entry-get pos "CLOSED")))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; Helper: Write timestamp range (NO SCHEDULED)
|
||||
;; ------------------------------------------------------------
|
||||
(defun gortium/internal--update-properties (pos start wknd id end task-end-map &optional fixed original-start)
|
||||
"Update WEEKEND_DAYS and write the time range at POS.
|
||||
For fixed tasks, ORIGINAL-START is preserved. OLD SCHEDULED/range lines are removed.
|
||||
The range is inserted directly below the heading, before any drawers."
|
||||
;; 3) If it's FIXED, use the existing range/scheduled time
|
||||
((and pos (string-equal "t" (org-entry-get pos "FIXED")))
|
||||
(or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback
|
||||
(org-get-scheduled-time pos)))
|
||||
|
||||
;; Otherwise: We MUST wait for this blocker to be recalculated
|
||||
(t nil))))
|
||||
|
||||
(if blocker-end
|
||||
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end))
|
||||
blocker-end latest-time))
|
||||
(setq all-resolved nil)))))
|
||||
(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-show-entry)
|
||||
|
||||
;; Update WEEKEND_DAYS property
|
||||
(if (and wknd (> wknd 0))
|
||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string wknd))
|
||||
(org-entry-delete (point) "WEEKEND_DAYS"))
|
||||
|
||||
;; Remove old ranges or SCHEDULED lines
|
||||
(save-excursion
|
||||
(org-back-to-heading t)
|
||||
(let ((limit (save-excursion (outline-next-heading) (point))))
|
||||
(forward-line 1)
|
||||
(while (< (point) limit)
|
||||
(cond
|
||||
((looking-at "^[ \t]*SCHEDULED:") (delete-region (point-at-bol) (1+ (point-at-eol))))
|
||||
((looking-at "^[ \t]*<.*>--<.*>") (delete-region (point-at-bol) (1+ (point-at-eol))))
|
||||
((looking-at "^\\*+") (goto-char limit))
|
||||
((looking-at "^[ \t]*:") (goto-char (line-beginning-position))
|
||||
(setq limit (point)))
|
||||
(t (forward-line 1))))))
|
||||
(let* ((subtree-start (point))
|
||||
(subtree-end (save-excursion (org-end-of-subtree t) (point))))
|
||||
|
||||
;; Determine insertion point: after heading, before drawers
|
||||
(let ((insert-point (save-excursion
|
||||
(org-back-to-heading t)
|
||||
(forward-line 1)
|
||||
(point)))
|
||||
(range-start (if fixed original-start start))) ;; preserve start if fixed
|
||||
;; Insert range
|
||||
(goto-char insert-point)
|
||||
(insert (format "%s--%s"
|
||||
(format-time-string "<%Y-%m-%d %a %H:%M>" range-start)
|
||||
(format-time-string "<%Y-%m-%d %a %H:%M>" end))
|
||||
"\n"))
|
||||
(save-restriction
|
||||
(narrow-to-region subtree-start subtree-end)
|
||||
|
||||
;; Update task-end-map
|
||||
;; --- 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)))
|
||||
|
||||
;; ------------------------------------------------------------
|
||||
;; MAIN FUNCTION
|
||||
;; 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 ()
|
||||
"Optimized scheduler: Ignores stale buffer ranges to ensure correct dependency flow."
|
||||
(interactive)
|
||||
(save-excursion
|
||||
(message "--- Starting Global Gantt Scheduler ---")
|
||||
(let ((all-tasks '())
|
||||
(message "=== Starting Gortium Scheduler ===")
|
||||
|
||||
;; 1. Global deactivations to prevent the "Parser Error"
|
||||
(let ((org-element-use-cache nil)
|
||||
(all-tasks '())
|
||||
(task-end-times (make-hash-table :test 'equal))
|
||||
(task-end-for-blockers (make-hash-table :test 'equal)))
|
||||
;; COLLECTION
|
||||
(start-time (current-time)))
|
||||
|
||||
;; 2. COLLECTION
|
||||
(org-map-entries
|
||||
(lambda ()
|
||||
(let ((state (org-get-todo-state)))
|
||||
(when state
|
||||
(when (org-get-todo-state)
|
||||
(let* ((pos (point-marker))
|
||||
(id (org-id-get-create))
|
||||
(effort (org-entry-get (point) "EFFORT"))
|
||||
(blocker (org-entry-get (point) "BLOCKER"))
|
||||
(fixed (org-entry-get (point) "FIXED"))
|
||||
(anchor (or (org-get-scheduled-time (point))
|
||||
(gortium/org--get-anchor-time)))
|
||||
(closed (org-entry-get (point) "CLOSED"))
|
||||
(offset (org-entry-get (point) "OFFSET_DAYS")))
|
||||
(when (or effort blocker fixed (string= state "DONE"))
|
||||
(push (list pos id effort blocker fixed anchor offset state closed)
|
||||
all-tasks))))))
|
||||
(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))
|
||||
|
||||
;; PASS 1: DONE & FIXED
|
||||
(dolist (task all-tasks)
|
||||
(pcase-let ((`(,pos ,id ,effort ,_ ,fixed ,anchor ,_ ,state ,closed) task))
|
||||
(cond
|
||||
;; DONE: compute range like normal, CLOSED is used for blockers
|
||||
((string= state "DONE")
|
||||
(let* ((start (or anchor (current-time)))
|
||||
(span (gortium/internal--calculate-task-span start effort)))
|
||||
(gortium/internal--update-properties pos (car span) (cadr span) id (car span)
|
||||
task-end-times))
|
||||
;; Store CLOSED for dependencies
|
||||
(when closed
|
||||
(puthash id (org-time-string-to-time closed) task-end-for-blockers)))
|
||||
;; FIXED tasks
|
||||
((and fixed anchor)
|
||||
(let* ((span (gortium/internal--calculate-task-span anchor effort)))
|
||||
(gortium/internal--update-properties pos anchor (cadr span) id (car span)
|
||||
task-end-times)))
|
||||
;; nothing else here
|
||||
)))
|
||||
|
||||
;; PASS 2: CHAINS
|
||||
(let ((remaining (cl-remove-if (lambda (t)
|
||||
(gethash (nth 1 t) task-end-times))
|
||||
all-tasks))
|
||||
(limit (* 5 (length all-tasks)))
|
||||
;; 3. THE LOOP
|
||||
(let* ((remaining all-tasks)
|
||||
(limit (* 20 (length remaining)))
|
||||
(iter 0))
|
||||
(while (and remaining (< iter limit))
|
||||
(cl-incf iter)
|
||||
(let ((done '()))
|
||||
(dolist (task remaining)
|
||||
(pcase-let ((`(,pos ,id ,effort ,blocker ,_ ,_ ,offset ,state ,closed) task))
|
||||
;; Determine dependency end
|
||||
(let* ((dep (or (gortium/internal--get-blocker-end blocker pos
|
||||
task-end-times)
|
||||
(and (string= state "DONE")
|
||||
(gethash id task-end-for-blockers)))))
|
||||
(when dep
|
||||
(let* ((off (if offset (string-to-number offset) 0))
|
||||
(start (gortium/internal--snap-to-working-hours
|
||||
(time-add dep (days-to-time off))))
|
||||
(span (gortium/internal--calculate-task-span start effort)))
|
||||
;; FIXED tasks: keep start
|
||||
(if (and (not (string= state "DONE")) (org-entry-get pos "FIXED"))
|
||||
(gortium/internal--update-properties pos anchor (cadr span) id (car span)
|
||||
task-end-times)
|
||||
(gortium/internal--update-properties pos (car span) (cadr span) id (car span)
|
||||
task-end-times))
|
||||
(push task done))))))
|
||||
(setq remaining (cl-set-difference remaining done)))))
|
||||
|
||||
(message "--- Scheduler Finished ---"))))
|
||||
(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)))))
|
||||
;; A task is ready if it's FIXED or all blockers are in the task-end-times map
|
||||
(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 (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
|
||||
(t (or blocker-end 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))))
|
||||
|
||||
(org-element-cache-reset 'all)
|
||||
(message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter))))
|
||||
|
||||
|
||||
;; ---------------------------------------------
|
||||
(defun gortium/org-ensure-task-properties ()
|
||||
@@ -1831,7 +1905,7 @@ and ensure the standard property drawer exists without overwriting existing data
|
||||
;; List of properties to ensure exist
|
||||
(props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS"
|
||||
"ASSIGNEE" "RESOURCES" "CATEGORY"
|
||||
"DIMENTIONS" "WEIGHT")))
|
||||
"DIMENTIONS" "WEIGHT" "OFFSET_DAYS")))
|
||||
(org-map-entries
|
||||
(lambda ()
|
||||
;; This check returns true if the heading has ANY todo keyword
|
||||
|
||||
Reference in New Issue
Block a user