WIP range for task instead of scheduled + ranges
This is to fix the double entry in the agenda. One for the scheduled + one for the range. We want only the range. But right now the code duplicate Ranges and PROPERTIES drawer.
This commit is contained in:
@@ -280,7 +280,7 @@ change ~org-directory~. It must be set before org loads!
|
|||||||
|
|
||||||
(setq org-global-properties
|
(setq org-global-properties
|
||||||
'(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
|
'(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
|
||||||
("ASSIGNEE_ALL" . "Thierry.P Cath.F Martin.K Michel.B Gabriel.C Silvie.L George.G Miguel.A Réjean.S Dominique Adnane Helper1 Helper2")))
|
("ASSIGNEE_ALL" . "Thierry.P Cath.F Martin.K Michel.B Gabriel.C Silvie.L George.G Miguel.A Réjean.S Dominique Adnane Hatim.K Marc-Antoine.P")))
|
||||||
|
|
||||||
(setq org-stuck-projects
|
(setq org-stuck-projects
|
||||||
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
||||||
@@ -466,9 +466,11 @@ change ~org-directory~. It must be set before org loads!
|
|||||||
)
|
)
|
||||||
("wP" "THE PLAN"
|
("wP" "THE PLAN"
|
||||||
((agenda ""
|
((agenda ""
|
||||||
((org-agenda-span 'month)
|
((org-agenda-span 60)
|
||||||
(org-agenda-start-day nil)
|
(org-agenda-start-day nil)
|
||||||
(org-agenda-overriding-header "📅 THE PLAN"))
|
(org-agenda-overriding-header "📅 THE PLAN")
|
||||||
|
(org-agenda-prefix-format " %?-12t%-12s")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
((org-agenda-tag-filter-preset '("+work")))
|
((org-agenda-tag-filter-preset '("+work")))
|
||||||
@@ -499,6 +501,9 @@ change ~org-directory~. It must be set before org loads!
|
|||||||
:matchers ("begin" "$1" "$" "$$" "\\(" "\\["))
|
:matchers ("begin" "$1" "$" "$$" "\\(" "\\["))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(after! ox-latex
|
||||||
|
(setq org-latex-compiler "xelatex"))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Org Capture
|
** Org Capture
|
||||||
@@ -573,8 +578,18 @@ fc --> UC3
|
|||||||
(setq elgantt--display-rules nil)
|
(setq elgantt--display-rules nil)
|
||||||
|
|
||||||
(defface gortium/elgantt-weekend-face
|
(defface gortium/elgantt-weekend-face
|
||||||
'((t (:background "#332222" :extend nil)))
|
'((t (:background "#32302f" :extend nil)))
|
||||||
"Face for weekend vertical columns in ElGantt.")
|
"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 ()
|
(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."
|
||||||
@@ -646,7 +661,7 @@ fc --> UC3
|
|||||||
(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 'outline
|
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
|
||||||
@@ -723,7 +738,7 @@ fc --> UC3
|
|||||||
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
||||||
(message "Added blocker: %s" new-id)))))))
|
(message "Added blocker: %s" new-id)))))))
|
||||||
|
|
||||||
;; --- 5. Blocker Lines (Multi-ID & Sync Support) ---
|
;; --- 5. Blocker Lines (Surgical Alignment Fix) ---
|
||||||
(elgantt-create-display-rule draw-blocker-lines
|
(elgantt-create-display-rule draw-blocker-lines
|
||||||
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
||||||
:args (elgantt-org-id)
|
:args (elgantt-org-id)
|
||||||
@@ -732,31 +747,34 @@ fc --> UC3
|
|||||||
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
||||||
(match-string 1 blocker-raw)
|
(match-string 1 blocker-raw)
|
||||||
blocker-raw))
|
blocker-raw))
|
||||||
;; Supports spaces or commas as separators
|
|
||||||
(id-list (split-string ids-string "[ ,]+" t)))
|
(id-list (split-string ids-string "[ ,]+" t)))
|
||||||
(dolist (blocker-id id-list)
|
(dolist (blocker-id id-list)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(when (elgantt--goto-id blocker-id)
|
(when (elgantt--goto-id blocker-id)
|
||||||
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
||||||
(list (org-entry-get (point) "SCHEDULED")
|
(list (org-entry-get (point) "SCHEDULED")
|
||||||
(org-entry-get (point) "EFFORT"))))
|
(org-entry-get (point) "EFFORT")
|
||||||
(b-sched (car blocker-data))
|
(org-entry-get (point) "WEEKEND_DAYS"))))
|
||||||
(b-effort (cadr blocker-data)))
|
(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)
|
(when (and b-sched b-effort)
|
||||||
(let* ((start-ts (ts-parse b-sched))
|
(let* ((start-ts (ts-parse b-sched))
|
||||||
(raw-mins (org-duration-to-minutes b-effort))
|
(raw-mins (org-duration-to-minutes b-effort))
|
||||||
(days-count (ceiling (/ (float raw-mins) 1440.0)))
|
;; Visual length must match the Effort Rule exactly
|
||||||
(p-start-base (save-excursion
|
(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))
|
(elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
|
||||||
(point))))
|
(point))))
|
||||||
(when (and (numberp p-start-base) (numberp p-blocked))
|
(when (and (numberp p-start) (numberp p-blocked))
|
||||||
;; FIX 1: compute p-line-start by date (handles "|" separators)
|
;; Point to the LAST DAY of the task bar
|
||||||
;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot
|
(let* ((end-date-ts (ts-adjust 'day (max 0 (1- total-days)) start-ts))
|
||||||
(let* ((end-ts (ts-adjust 'day (- days-count 2) start-ts))
|
|
||||||
(p-line-start (save-excursion
|
(p-line-start (save-excursion
|
||||||
(elgantt--goto-date (ts-format "%Y-%m-%d" end-ts))
|
(elgantt--goto-date (ts-format "%Y-%m-%d" end-date-ts))
|
||||||
(point))))
|
(point))))
|
||||||
(when (numberp p-line-start)
|
(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)
|
(elgantt--draw-line (truncate p-line-start)
|
||||||
(truncate p-blocked)
|
(truncate p-blocked)
|
||||||
"#b8bb26"))))))))))))))
|
"#b8bb26"))))))))))))))
|
||||||
@@ -786,7 +804,7 @@ fc --> UC3
|
|||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
#+RESULTS:
|
#+RESULTS:
|
||||||
: elgantt--action-rule-follow-hashtag-link-backward
|
: elgantt-open-current-org-file
|
||||||
|
|
||||||
* Org Roam
|
* Org Roam
|
||||||
|
|
||||||
@@ -2191,216 +2209,297 @@ If FILE is nil, refile in the current file."
|
|||||||
* Custom function for reschedulling
|
* Custom function for reschedulling
|
||||||
|
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
(defun gortium/add-trigger-scheduling-next ()
|
;;; ============================================================
|
||||||
"Add scheduled chain for this entry."
|
;;; GORTIUM — Org chain scheduler (timestamp-based, no duplicates)
|
||||||
(interactive)
|
;;; ============================================================
|
||||||
(org-set-property "TRIGGER" "next-sibling scheduled!(\"++0d\") todo!(NEXT) chain!(\"TRIGGER\")"))
|
|
||||||
|
|
||||||
(defun gortium/org-schedule-after-previous-sibling ()
|
(require 'org)
|
||||||
"Schedule the current task right after its previous sibling.
|
(require 'org-id)
|
||||||
If the sibling is DONE, use its CLOSED time.
|
(require 'cl-lib)
|
||||||
Otherwise, use SCHEDULED + EFFORT."
|
(require 's)
|
||||||
(interactive)
|
|
||||||
(unless (org-at-heading-p)
|
;; ------------------------------------------------------------
|
||||||
(org-back-to-heading t))
|
;; Helper: Skip weekends (snap to next weekday at 08:00)
|
||||||
(let ((current-level (org-current-level))
|
;; ------------------------------------------------------------
|
||||||
(current-point (point))
|
(defun gortium/org--skip-weekend (time)
|
||||||
prev-end-time)
|
(let* ((decoded (decode-time time))
|
||||||
(save-excursion
|
(dow (nth 6 decoded)))
|
||||||
;; Find previous sibling of same level
|
|
||||||
(let ((found nil))
|
|
||||||
(while (and (not found) (outline-previous-heading))
|
|
||||||
(when (= (org-current-level) current-level)
|
|
||||||
(setq found t)))
|
|
||||||
(unless found
|
|
||||||
(user-error "No previous sibling found"))
|
|
||||||
;; At previous sibling now
|
|
||||||
(let* ((state (org-get-todo-state))
|
|
||||||
(scheduled (org-entry-get nil "SCHEDULED"))
|
|
||||||
(closed (org-entry-get nil "CLOSED"))
|
|
||||||
(effort (org-entry-get nil "EFFORT")))
|
|
||||||
(cond
|
(cond
|
||||||
;; If task is DONE and CLOSED exists
|
((= dow 6) ;; Saturday
|
||||||
((and (member state org-done-keywords) closed)
|
(apply #'encode-time
|
||||||
(setq prev-end-time (org-time-string-to-time closed)))
|
(append '(0 0 8)
|
||||||
;; Else use SCHEDULED + EFFORT
|
(nthcdr 3 (decode-time (time-add time (days-to-time 2)))))))
|
||||||
((and scheduled effort)
|
((= dow 0) ;; Sunday
|
||||||
(let* ((sched-time (org-time-string-to-time scheduled))
|
(apply #'encode-time
|
||||||
(duration-min (org-duration-to-minutes effort)))
|
(append '(0 0 8)
|
||||||
(setq prev-end-time
|
(nthcdr 3 (decode-time (time-add time (days-to-time 1)))))))
|
||||||
(time-add sched-time (seconds-to-time (* 60 duration-min))))))
|
(t time))))
|
||||||
(t
|
|
||||||
(user-error "Previous sibling missing SCHEDULED/CLOSED or EFFORT"))))))
|
|
||||||
;; Schedule current task
|
|
||||||
(goto-char current-point)
|
|
||||||
(org-schedule nil (format-time-string (org-time-stamp-format t) prev-end-time))))
|
|
||||||
|
|
||||||
;; --- Helper: Snap to working hours (8-16) ---
|
;; ------------------------------------------------------------
|
||||||
;; --- Helper: Snap to working hours (8-16) ---
|
;; Helper: Snap to working hours (08:00–16:00)
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--snap-to-working-hours (time)
|
(defun gortium/internal--snap-to-working-hours (time)
|
||||||
(let* ((day-start 8) (day-end 16)
|
(let* ((day-start 8)
|
||||||
|
(day-end 16)
|
||||||
(t1 (gortium/org--skip-weekend time))
|
(t1 (gortium/org--skip-weekend time))
|
||||||
(d (decode-time t1)) (h (nth 2 d)))
|
(d (decode-time t1))
|
||||||
|
(h (nth 2 d)))
|
||||||
(cond
|
(cond
|
||||||
((< h day-start) (apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
|
((< h day-start)
|
||||||
|
(apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
|
||||||
((>= h day-end)
|
((>= h day-end)
|
||||||
(let ((next (time-add t1 (days-to-time 1))))
|
(let ((next (time-add t1 (days-to-time 1))))
|
||||||
(gortium/org--skip-weekend (apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 (decode-time next)))))))
|
(gortium/org--skip-weekend
|
||||||
|
(apply #'encode-time (append (list 0 0 day-start)
|
||||||
|
(nthcdr 3 (decode-time next)))))))
|
||||||
(t t1))))
|
(t t1))))
|
||||||
|
|
||||||
;; --- Helper: Calculate End Time & Weekend Jump (THE WALKER FIX) ---
|
;; ------------------------------------------------------------
|
||||||
|
;; Helper: Calculate task span using EFFORT (hour-grained)
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--calculate-task-span (start-time effort-str)
|
(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))
|
(if (or (null effort-str) (string-empty-p effort-str))
|
||||||
(list start-time 0)
|
(list start-time 0)
|
||||||
(let* ((eff-mins (org-duration-to-minutes effort-str))
|
(let* ((eff-mins (org-duration-to-minutes effort-str))
|
||||||
;; SMART MATH: If string has "d", divide by 1440 (24h). If only "h", divide by 480 (8h).
|
(total-work-mins (if (string-match-p "d" effort-str)
|
||||||
(divisor (if (string-match-p "d" effort-str) 1440.0 480.0))
|
(* (/ eff-mins 1440.0) 480)
|
||||||
(eff-days (max 1 (ceiling (/ (float eff-mins) divisor))))
|
eff-mins))
|
||||||
(cursor start-time)
|
(cursor start-time)
|
||||||
(days-worked 0)
|
(wknd-count 0)
|
||||||
(wknd-count 0))
|
(day-start 8)
|
||||||
|
(day-end 16))
|
||||||
;; THE WALKER: Simulate the task day by day
|
(while (> total-work-mins 0)
|
||||||
(while (< days-worked eff-days)
|
(let* ((decoded (decode-time cursor))
|
||||||
(let* ((dow (nth 6 (decode-time cursor))))
|
(h (nth 2 decoded))
|
||||||
|
(m (nth 1 decoded))
|
||||||
|
(dow (nth 6 decoded))
|
||||||
|
(current-abs-min (+ (* h 60) m))
|
||||||
|
(day-end-abs-min (* day-end 60))
|
||||||
|
(mins-left-today (- day-end-abs-min current-abs-min)))
|
||||||
(cond
|
(cond
|
||||||
;; If Sat (6) or Sun (0), it's a weekend. Count it, but don't advance work.
|
((or (= dow 6) (= dow 0)) ;; weekend
|
||||||
((or (= dow 6) (= dow 0))
|
|
||||||
(setq wknd-count (1+ wknd-count))
|
(setq wknd-count (1+ wknd-count))
|
||||||
(setq cursor (time-add cursor (days-to-time 1))))
|
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||||||
;; If Mon-Fri, it's a work day. Advance work count.
|
(nthcdr 3 (decode-time
|
||||||
(t
|
(time-add cursor (days-to-time 1))))))))
|
||||||
(setq days-worked (1+ days-worked))
|
((<= mins-left-today 0) ;; after hours
|
||||||
;; Move cursor to start of next day
|
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||||||
(setq cursor (time-add cursor (days-to-time 1)))))))
|
(nthcdr 3 (decode-time
|
||||||
|
(time-add cursor (days-to-time 1))))))))
|
||||||
;; Return [End-Time, Weekend-Days-Count]
|
((<= total-work-mins mins-left-today)
|
||||||
|
(setq cursor (time-add cursor (seconds-to-time (* total-work-mins 60))))
|
||||||
|
(setq total-work-mins 0))
|
||||||
|
(t ;; spill to next day
|
||||||
|
(setq total-work-mins (- total-work-mins mins-left-today))
|
||||||
|
(setq cursor (time-add cursor (seconds-to-time (* mins-left-today 60))))))))
|
||||||
(list cursor wknd-count))))
|
(list cursor wknd-count))))
|
||||||
|
|
||||||
;; --- Helper: Find Blocker End Time ---
|
;; ------------------------------------------------------------
|
||||||
|
;; Helper: Find dependency end time
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--get-blocker-end (blocker-str current-pos task-end-map)
|
(defun gortium/internal--get-blocker-end (blocker-str current-pos task-end-map)
|
||||||
(let ((clean (s-trim blocker-str)) (latest-time nil))
|
"Return the latest end time among the dependencies listed in BLOCKER-STR."
|
||||||
|
(let ((clean (s-trim (format "%s" blocker-str)))
|
||||||
|
(latest-time nil))
|
||||||
(cond
|
(cond
|
||||||
((string-match-p "previous-sibling" clean)
|
((string-match-p "previous-sibling" clean)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char current-pos)
|
(goto-char current-pos)
|
||||||
(let ((found nil))
|
(let ((found nil))
|
||||||
(while (and (not found) (org-get-last-sibling))
|
(while (and (not found) (org-get-last-sibling))
|
||||||
(let* ((sid (org-id-get)) (end (when sid (gethash sid task-end-map))))
|
(let* ((sid (org-id-get))
|
||||||
(when end (setq latest-time end) (setq found t)))))))
|
(end (when sid (gethash sid task-end-map))))
|
||||||
|
(when end
|
||||||
|
(setq latest-time end
|
||||||
|
found t)))))))
|
||||||
((string-match-p "parent" clean)
|
((string-match-p "parent" clean)
|
||||||
(save-excursion (goto-char current-pos)
|
(save-excursion
|
||||||
|
(goto-char current-pos)
|
||||||
(when (org-up-heading-safe)
|
(when (org-up-heading-safe)
|
||||||
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
||||||
((string-match "ids(\\(.*?\\))" clean)
|
((string-match "ids(\\(.*?\\))" clean)
|
||||||
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
|
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
|
||||||
(let ((tend (gethash (replace-regexp-in-string "[\"']\\|id:" "" tid) task-end-map)))
|
(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)))
|
(when (and tend
|
||||||
|
(or (null latest-time)
|
||||||
|
(time-less-p latest-time tend)))
|
||||||
(setq latest-time tend))))))
|
(setq latest-time tend))))))
|
||||||
latest-time))
|
latest-time))
|
||||||
|
|
||||||
;; --- Helper: Write Properties (Safe) ---
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--update-properties (pos start wknd id end task-end-map)
|
;; Helper: Get earliest timestamp in entry
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
(defun gortium/org--get-anchor-time ()
|
||||||
|
"Return the earliest timestamp in current entry, or nil."
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char pos) ;; POS is a MARKER
|
(org-back-to-heading t)
|
||||||
(if (and wknd (> wknd 0))
|
(let ((limit (save-excursion (outline-next-heading) (point)))
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string wknd))
|
(best nil))
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" nil))
|
(while (re-search-forward org-ts-regexp-both limit t 1)
|
||||||
(org-schedule nil (format-time-string "<%Y-%m-%d %a %H:%M>" start))
|
(let ((ts (org-time-string-to-time (match-string 0))))
|
||||||
(puthash id end task-end-map)))
|
(when (or (null best) (time-less-p ts best))
|
||||||
|
(setq best ts))))
|
||||||
|
best)))
|
||||||
|
|
||||||
(defun gortium/org--skip-weekend (time)
|
;; ------------------------------------------------------------
|
||||||
"Advance TIME to the next Monday morning if it falls on a weekend."
|
;; Helper: Write timestamp range (NO SCHEDULED)
|
||||||
(let* ((decoded (decode-time time))
|
;; ------------------------------------------------------------
|
||||||
(dow (nth 6 decoded)))
|
(defun gortium/internal--update-properties (pos start wknd id end task-end-map &optional fixed original-start)
|
||||||
(cond
|
"Update WEEKEND_DAYS and write the time range at POS.
|
||||||
((= dow 6) ;; Saturday
|
For fixed tasks, ORIGINAL-START is preserved. OLD SCHEDULED/range lines are removed.
|
||||||
(let ((next (time-add time (days-to-time 2))))
|
The range is inserted directly below the heading, before any drawers."
|
||||||
(apply #'encode-time (append '(0 0 8) (nthcdr 3 (decode-time next))))))
|
|
||||||
((= dow 0) ;; Sunday
|
|
||||||
(let ((next (time-add time (days-to-time 1))))
|
|
||||||
(apply #'encode-time (append '(0 0 8) (nthcdr 3 (decode-time next))))))
|
|
||||||
(t time))))
|
|
||||||
|
|
||||||
;; --- MAIN FUNCTION ---
|
|
||||||
(defun gortium/org-schedule-subtree-chains ()
|
|
||||||
"Schedule tasks using MARKERS and WALKER logic."
|
|
||||||
(interactive)
|
|
||||||
(save-excursion
|
|
||||||
(message "--- Starting Gantt Scheduler ---")
|
|
||||||
(let* ((all-tasks '()) (task-end-times (make-hash-table :test 'equal)))
|
|
||||||
|
|
||||||
;; Collection
|
|
||||||
(org-map-entries
|
|
||||||
(lambda ()
|
|
||||||
(when (org-get-todo-state) ;; Strict TODO check
|
|
||||||
(let ((effort (org-entry-get (point) "EFFORT" nil))
|
|
||||||
(blocker (org-entry-get (point) "BLOCKER" nil))
|
|
||||||
(fixed (org-entry-get (point) "FIXED" nil))
|
|
||||||
(scheduled (org-entry-get (point) "SCHEDULED")))
|
|
||||||
(when (or effort blocker fixed)
|
|
||||||
(push (list (point-marker) (org-id-get-create) (org-get-heading t t t t)
|
|
||||||
effort (when blocker (s-trim blocker)) fixed scheduled
|
|
||||||
(org-entry-get (point) "OFFSET_DAYS" nil)) all-tasks)))))
|
|
||||||
nil (if (org-at-heading-p) 'tree nil))
|
|
||||||
(setq all-tasks (nreverse all-tasks))
|
|
||||||
|
|
||||||
;; Pass 1: FIXED
|
|
||||||
(dolist (task all-tasks)
|
|
||||||
(pcase-let ((`(,pos ,id ,heading ,effort ,blocker ,fixed ,scheduled ,offset) task))
|
|
||||||
(when (and fixed (member (downcase fixed) '("t" "true" "yes")))
|
|
||||||
(if scheduled
|
|
||||||
(let* ((start (org-time-string-to-time scheduled))
|
|
||||||
(span (gortium/internal--calculate-task-span start effort)))
|
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char pos)
|
(goto-char pos)
|
||||||
(if (and (cadr span) (> (cadr span) 0))
|
(org-show-entry)
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string (cadr span)))
|
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" nil))
|
|
||||||
(puthash id (car span) task-end-times)))
|
|
||||||
(message "WARNING: Fixed task '%s' missing date." heading)))))
|
|
||||||
|
|
||||||
;; Pass 2: CHAINS
|
;; Update WEEKEND_DAYS property
|
||||||
(let* ((remaining (cl-remove-if (lambda (tk) (or (nth 5 tk) (not (nth 4 tk)))) all-tasks))
|
(if (and wknd (> wknd 0))
|
||||||
(iter 0) (limit (* 5 (length all-tasks))))
|
(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))))))
|
||||||
|
|
||||||
|
;; 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"))
|
||||||
|
|
||||||
|
;; Update task-end-map
|
||||||
|
(puthash id end task-end-map)))
|
||||||
|
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
;; MAIN FUNCTION
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
(defun gortium/org-schedule-subtree-chains ()
|
||||||
|
(interactive)
|
||||||
|
(save-excursion
|
||||||
|
(message "--- Starting Global Gantt Scheduler ---")
|
||||||
|
(let ((all-tasks '())
|
||||||
|
(task-end-times (make-hash-table :test 'equal))
|
||||||
|
(task-end-for-blockers (make-hash-table :test 'equal)))
|
||||||
|
;; COLLECTION
|
||||||
|
(org-map-entries
|
||||||
|
(lambda ()
|
||||||
|
(let ((state (org-get-todo-state)))
|
||||||
|
(when 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))))))
|
||||||
|
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)))
|
||||||
|
(iter 0))
|
||||||
(while (and remaining (< iter limit))
|
(while (and remaining (< iter limit))
|
||||||
(cl-incf iter)
|
(cl-incf iter)
|
||||||
(let ((scheduled-this-loop '()))
|
(let ((done '()))
|
||||||
(dolist (task remaining)
|
(dolist (task remaining)
|
||||||
(pcase-let ((`(,pos ,id ,heading ,effort ,blocker ,fixed ,scheduled ,offset) task))
|
(pcase-let ((`(,pos ,id ,effort ,blocker ,_ ,_ ,offset ,state ,closed) task))
|
||||||
(let ((dep-end (gortium/internal--get-blocker-end blocker pos task-end-times)))
|
;; Determine dependency end
|
||||||
(when dep-end
|
(let* ((dep (or (gortium/internal--get-blocker-end blocker pos
|
||||||
(let* ((off-val (if offset (string-to-number offset) 0))
|
task-end-times)
|
||||||
(base-start (time-add dep-end (days-to-time off-val)))
|
(and (string= state "DONE")
|
||||||
(start (gortium/internal--snap-to-working-hours base-start))
|
(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)))
|
(span (gortium/internal--calculate-task-span start effort)))
|
||||||
(gortium/internal--update-properties pos start (cadr span) id (car span) task-end-times)
|
;; FIXED tasks: keep start
|
||||||
(push task scheduled-this-loop))))))
|
(if (and (not (string= state "DONE")) (org-entry-get pos "FIXED"))
|
||||||
(setq remaining (cl-remove-if (lambda (tk) (member tk scheduled-this-loop)) remaining))))
|
(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)))))
|
||||||
|
|
||||||
(dolist (task all-tasks) (set-marker (car task) nil)) ;; Clean markers
|
(message "--- Scheduler Finished ---"))))
|
||||||
(message "--- Scheduler Finished ---")))))
|
|
||||||
|
|
||||||
;; USAGE:
|
;; ---------------------------------------------
|
||||||
;; 1. Set up your tasks with either:
|
(defun gortium/org-ensure-task-properties ()
|
||||||
;; - :FIXED: t and a SCHEDULED date, OR
|
"Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)
|
||||||
;; - :BLOCKER: previous-sibling / ids(UUID) / ids("id:UUID") / parent
|
and ensure the standard property drawer exists without overwriting existing data."
|
||||||
;; 2. Put cursor on "Planning" heading
|
(interactive)
|
||||||
;; 3. M-x gortium/org-schedule-subtree-chains
|
(save-excursion
|
||||||
;;
|
(message "--- Initializing Task Properties for all states ---")
|
||||||
;; The function will:
|
(let ((count 0)
|
||||||
;; - Use FIXED tasks as anchors
|
;; List of properties to ensure exist
|
||||||
;; - Calculate all other tasks from their dependencies
|
(props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS"
|
||||||
;; - Warn about tasks without BLOCKER or FIXED
|
"ASSIGNEE" "RESOURCES" "CATEGORY"
|
||||||
;; - Detect circular dependencies
|
"DIMENTIONS" "WEIGHT")))
|
||||||
;; - Respect 8-hour workday limits
|
(org-map-entries
|
||||||
;; - Skip weekends
|
(lambda ()
|
||||||
;;
|
;; This check returns true if the heading has ANY todo keyword
|
||||||
;; Supported BLOCKER types (standard EDNA format):
|
(let ((todo-state (org-get-todo-state)))
|
||||||
;; - previous-sibling
|
(when todo-state
|
||||||
;; - ids(UUID) - e.g., ids(70d8f844-1952-43f0-8043-da51e8c8bc69)
|
(cl-incf count)
|
||||||
;; - ids("id:UUID") - e.g., ids("id:70d8f844-1952-43f0-8043-da51e8c8bc69")
|
;; 1. Handle ID (builtin handles 'do not replace' logic)
|
||||||
;; - parent
|
(org-id-get-create)
|
||||||
|
|
||||||
|
;; 2. Ensure all other keys exist
|
||||||
|
(dolist (prop props)
|
||||||
|
;; Only add the property if it's currently nil/missing
|
||||||
|
(unless (org-entry-get (point) prop)
|
||||||
|
(org-entry-put (point) prop
|
||||||
|
(if (string= prop "FIXED") "nil" "")))))))
|
||||||
|
nil nil)
|
||||||
|
(message "--- Finished: Processed %d tasks across all states ---" count))))
|
||||||
|
|
||||||
(defun gortium/add-ids-to-subtree ()
|
(defun gortium/add-ids-to-subtree ()
|
||||||
"Add IDs to all headings in current subtree."
|
"Add IDs to all headings in current subtree."
|
||||||
@@ -2410,22 +2509,11 @@ Otherwise, use SCHEDULED + EFFORT."
|
|||||||
(org-map-entries
|
(org-map-entries
|
||||||
(lambda () (org-id-get-create))
|
(lambda () (org-id-get-create))
|
||||||
nil 'tree)))
|
nil 'tree)))
|
||||||
|
|
||||||
;; Custom function to shift projects
|
|
||||||
(defun gortium/org-shift-subtree-schedules (days)
|
|
||||||
"Shift all SCHEDULED dates in the current subtree by DAYS."
|
|
||||||
(interactive "nDays to shift: ")
|
|
||||||
(org-map-entries
|
|
||||||
(lambda ()
|
|
||||||
(let ((scheduled (org-entry-get nil "SCHEDULED")))
|
|
||||||
(when scheduled
|
|
||||||
(let* ((time (org-time-string-to-time scheduled))
|
|
||||||
(new-time (time-add time (days-to-time days)))
|
|
||||||
(new-date (format-time-string (org-time-stamp-format) new-time)))
|
|
||||||
(org-entry-put nil "SCHEDULED" new-date)))))
|
|
||||||
nil 'tree))
|
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: gortium/add-ids-to-subtree
|
||||||
|
|
||||||
* Custom function Open link in other frame
|
* Custom function Open link in other frame
|
||||||
|
|
||||||
#+begin_src emacs-lisp
|
#+begin_src emacs-lisp
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
(setq org-global-properties
|
(setq org-global-properties
|
||||||
'(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
|
'(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
|
||||||
("ASSIGNEE_ALL" . "Thierry.P Cath.F Martin.K Michel.B Gabriel.C Silvie.L George.G Miguel.A Réjean.S Dominique Adnane Helper1 Helper2")))
|
("ASSIGNEE_ALL" . "Thierry.P Cath.F Martin.K Michel.B Gabriel.C Silvie.L George.G Miguel.A Réjean.S Dominique Adnane Hatim.K Marc-Antoine.P")))
|
||||||
|
|
||||||
(setq org-stuck-projects
|
(setq org-stuck-projects
|
||||||
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
||||||
@@ -336,9 +336,11 @@
|
|||||||
)
|
)
|
||||||
("wP" "THE PLAN"
|
("wP" "THE PLAN"
|
||||||
((agenda ""
|
((agenda ""
|
||||||
((org-agenda-span 'month)
|
((org-agenda-span 60)
|
||||||
(org-agenda-start-day nil)
|
(org-agenda-start-day nil)
|
||||||
(org-agenda-overriding-header "📅 THE PLAN"))
|
(org-agenda-overriding-header "📅 THE PLAN")
|
||||||
|
(org-agenda-prefix-format " %?-12t%-12s")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
((org-agenda-tag-filter-preset '("+work")))
|
((org-agenda-tag-filter-preset '("+work")))
|
||||||
@@ -362,6 +364,9 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(after! ox-latex
|
||||||
|
(setq org-latex-compiler "xelatex"))
|
||||||
|
|
||||||
(after! org
|
(after! org
|
||||||
(setq
|
(setq
|
||||||
org-capture-templates
|
org-capture-templates
|
||||||
@@ -401,8 +406,18 @@
|
|||||||
(setq elgantt--display-rules nil)
|
(setq elgantt--display-rules nil)
|
||||||
|
|
||||||
(defface gortium/elgantt-weekend-face
|
(defface gortium/elgantt-weekend-face
|
||||||
'((t (:background "#332222" :extend nil)))
|
'((t (:background "#32302f" :extend nil)))
|
||||||
"Face for weekend vertical columns in ElGantt.")
|
"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 ()
|
(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."
|
||||||
@@ -474,7 +489,7 @@
|
|||||||
(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 'outline
|
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
|
||||||
@@ -551,7 +566,7 @@
|
|||||||
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
(org-set-property "BLOCKER" (format "ids(%s)" new-id)))
|
||||||
(message "Added blocker: %s" new-id)))))))
|
(message "Added blocker: %s" new-id)))))))
|
||||||
|
|
||||||
;; --- 5. Blocker Lines (Multi-ID & Sync Support) ---
|
;; --- 5. Blocker Lines (Surgical Alignment Fix) ---
|
||||||
(elgantt-create-display-rule draw-blocker-lines
|
(elgantt-create-display-rule draw-blocker-lines
|
||||||
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
:parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
||||||
:args (elgantt-org-id)
|
:args (elgantt-org-id)
|
||||||
@@ -560,31 +575,34 @@
|
|||||||
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
(ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw)
|
||||||
(match-string 1 blocker-raw)
|
(match-string 1 blocker-raw)
|
||||||
blocker-raw))
|
blocker-raw))
|
||||||
;; Supports spaces or commas as separators
|
|
||||||
(id-list (split-string ids-string "[ ,]+" t)))
|
(id-list (split-string ids-string "[ ,]+" t)))
|
||||||
(dolist (blocker-id id-list)
|
(dolist (blocker-id id-list)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(when (elgantt--goto-id blocker-id)
|
(when (elgantt--goto-id blocker-id)
|
||||||
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
(let* ((blocker-data (elgantt-with-point-at-orig-entry nil
|
||||||
(list (org-entry-get (point) "SCHEDULED")
|
(list (org-entry-get (point) "SCHEDULED")
|
||||||
(org-entry-get (point) "EFFORT"))))
|
(org-entry-get (point) "EFFORT")
|
||||||
(b-sched (car blocker-data))
|
(org-entry-get (point) "WEEKEND_DAYS"))))
|
||||||
(b-effort (cadr blocker-data)))
|
(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)
|
(when (and b-sched b-effort)
|
||||||
(let* ((start-ts (ts-parse b-sched))
|
(let* ((start-ts (ts-parse b-sched))
|
||||||
(raw-mins (org-duration-to-minutes b-effort))
|
(raw-mins (org-duration-to-minutes b-effort))
|
||||||
(days-count (ceiling (/ (float raw-mins) 1440.0)))
|
;; Visual length must match the Effort Rule exactly
|
||||||
(p-start-base (save-excursion
|
(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))
|
(elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
|
||||||
(point))))
|
(point))))
|
||||||
(when (and (numberp p-start-base) (numberp p-blocked))
|
(when (and (numberp p-start) (numberp p-blocked))
|
||||||
;; FIX 1: compute p-line-start by date (handles "|" separators)
|
;; Point to the LAST DAY of the task bar
|
||||||
;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot
|
(let* ((end-date-ts (ts-adjust 'day (max 0 (1- total-days)) start-ts))
|
||||||
(let* ((end-ts (ts-adjust 'day (- days-count 2) start-ts))
|
|
||||||
(p-line-start (save-excursion
|
(p-line-start (save-excursion
|
||||||
(elgantt--goto-date (ts-format "%Y-%m-%d" end-ts))
|
(elgantt--goto-date (ts-format "%Y-%m-%d" end-date-ts))
|
||||||
(point))))
|
(point))))
|
||||||
(when (numberp p-line-start)
|
(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)
|
(elgantt--draw-line (truncate p-line-start)
|
||||||
(truncate p-blocked)
|
(truncate p-blocked)
|
||||||
"#b8bb26"))))))))))))))
|
"#b8bb26"))))))))))))))
|
||||||
@@ -1540,216 +1558,297 @@ If FILE is nil, refile in the current file."
|
|||||||
|
|
||||||
(add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer)
|
(add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer)
|
||||||
|
|
||||||
(defun gortium/add-trigger-scheduling-next ()
|
;;; ============================================================
|
||||||
"Add scheduled chain for this entry."
|
;;; GORTIUM — Org chain scheduler (timestamp-based, no duplicates)
|
||||||
(interactive)
|
;;; ============================================================
|
||||||
(org-set-property "TRIGGER" "next-sibling scheduled!(\"++0d\") todo!(NEXT) chain!(\"TRIGGER\")"))
|
|
||||||
|
|
||||||
(defun gortium/org-schedule-after-previous-sibling ()
|
(require 'org)
|
||||||
"Schedule the current task right after its previous sibling.
|
(require 'org-id)
|
||||||
If the sibling is DONE, use its CLOSED time.
|
(require 'cl-lib)
|
||||||
Otherwise, use SCHEDULED + EFFORT."
|
(require 's)
|
||||||
(interactive)
|
|
||||||
(unless (org-at-heading-p)
|
;; ------------------------------------------------------------
|
||||||
(org-back-to-heading t))
|
;; Helper: Skip weekends (snap to next weekday at 08:00)
|
||||||
(let ((current-level (org-current-level))
|
;; ------------------------------------------------------------
|
||||||
(current-point (point))
|
(defun gortium/org--skip-weekend (time)
|
||||||
prev-end-time)
|
(let* ((decoded (decode-time time))
|
||||||
(save-excursion
|
(dow (nth 6 decoded)))
|
||||||
;; Find previous sibling of same level
|
|
||||||
(let ((found nil))
|
|
||||||
(while (and (not found) (outline-previous-heading))
|
|
||||||
(when (= (org-current-level) current-level)
|
|
||||||
(setq found t)))
|
|
||||||
(unless found
|
|
||||||
(user-error "No previous sibling found"))
|
|
||||||
;; At previous sibling now
|
|
||||||
(let* ((state (org-get-todo-state))
|
|
||||||
(scheduled (org-entry-get nil "SCHEDULED"))
|
|
||||||
(closed (org-entry-get nil "CLOSED"))
|
|
||||||
(effort (org-entry-get nil "EFFORT")))
|
|
||||||
(cond
|
(cond
|
||||||
;; If task is DONE and CLOSED exists
|
((= dow 6) ;; Saturday
|
||||||
((and (member state org-done-keywords) closed)
|
(apply #'encode-time
|
||||||
(setq prev-end-time (org-time-string-to-time closed)))
|
(append '(0 0 8)
|
||||||
;; Else use SCHEDULED + EFFORT
|
(nthcdr 3 (decode-time (time-add time (days-to-time 2)))))))
|
||||||
((and scheduled effort)
|
((= dow 0) ;; Sunday
|
||||||
(let* ((sched-time (org-time-string-to-time scheduled))
|
(apply #'encode-time
|
||||||
(duration-min (org-duration-to-minutes effort)))
|
(append '(0 0 8)
|
||||||
(setq prev-end-time
|
(nthcdr 3 (decode-time (time-add time (days-to-time 1)))))))
|
||||||
(time-add sched-time (seconds-to-time (* 60 duration-min))))))
|
(t time))))
|
||||||
(t
|
|
||||||
(user-error "Previous sibling missing SCHEDULED/CLOSED or EFFORT"))))))
|
|
||||||
;; Schedule current task
|
|
||||||
(goto-char current-point)
|
|
||||||
(org-schedule nil (format-time-string (org-time-stamp-format t) prev-end-time))))
|
|
||||||
|
|
||||||
;; --- Helper: Snap to working hours (8-16) ---
|
;; ------------------------------------------------------------
|
||||||
;; --- Helper: Snap to working hours (8-16) ---
|
;; Helper: Snap to working hours (08:00–16:00)
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--snap-to-working-hours (time)
|
(defun gortium/internal--snap-to-working-hours (time)
|
||||||
(let* ((day-start 8) (day-end 16)
|
(let* ((day-start 8)
|
||||||
|
(day-end 16)
|
||||||
(t1 (gortium/org--skip-weekend time))
|
(t1 (gortium/org--skip-weekend time))
|
||||||
(d (decode-time t1)) (h (nth 2 d)))
|
(d (decode-time t1))
|
||||||
|
(h (nth 2 d)))
|
||||||
(cond
|
(cond
|
||||||
((< h day-start) (apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
|
((< h day-start)
|
||||||
|
(apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
|
||||||
((>= h day-end)
|
((>= h day-end)
|
||||||
(let ((next (time-add t1 (days-to-time 1))))
|
(let ((next (time-add t1 (days-to-time 1))))
|
||||||
(gortium/org--skip-weekend (apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 (decode-time next)))))))
|
(gortium/org--skip-weekend
|
||||||
|
(apply #'encode-time (append (list 0 0 day-start)
|
||||||
|
(nthcdr 3 (decode-time next)))))))
|
||||||
(t t1))))
|
(t t1))))
|
||||||
|
|
||||||
;; --- Helper: Calculate End Time & Weekend Jump (THE WALKER FIX) ---
|
;; ------------------------------------------------------------
|
||||||
|
;; Helper: Calculate task span using EFFORT (hour-grained)
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--calculate-task-span (start-time effort-str)
|
(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))
|
(if (or (null effort-str) (string-empty-p effort-str))
|
||||||
(list start-time 0)
|
(list start-time 0)
|
||||||
(let* ((eff-mins (org-duration-to-minutes effort-str))
|
(let* ((eff-mins (org-duration-to-minutes effort-str))
|
||||||
;; SMART MATH: If string has "d", divide by 1440 (24h). If only "h", divide by 480 (8h).
|
(total-work-mins (if (string-match-p "d" effort-str)
|
||||||
(divisor (if (string-match-p "d" effort-str) 1440.0 480.0))
|
(* (/ eff-mins 1440.0) 480)
|
||||||
(eff-days (max 1 (ceiling (/ (float eff-mins) divisor))))
|
eff-mins))
|
||||||
(cursor start-time)
|
(cursor start-time)
|
||||||
(days-worked 0)
|
(wknd-count 0)
|
||||||
(wknd-count 0))
|
(day-start 8)
|
||||||
|
(day-end 16))
|
||||||
;; THE WALKER: Simulate the task day by day
|
(while (> total-work-mins 0)
|
||||||
(while (< days-worked eff-days)
|
(let* ((decoded (decode-time cursor))
|
||||||
(let* ((dow (nth 6 (decode-time cursor))))
|
(h (nth 2 decoded))
|
||||||
|
(m (nth 1 decoded))
|
||||||
|
(dow (nth 6 decoded))
|
||||||
|
(current-abs-min (+ (* h 60) m))
|
||||||
|
(day-end-abs-min (* day-end 60))
|
||||||
|
(mins-left-today (- day-end-abs-min current-abs-min)))
|
||||||
(cond
|
(cond
|
||||||
;; If Sat (6) or Sun (0), it's a weekend. Count it, but don't advance work.
|
((or (= dow 6) (= dow 0)) ;; weekend
|
||||||
((or (= dow 6) (= dow 0))
|
|
||||||
(setq wknd-count (1+ wknd-count))
|
(setq wknd-count (1+ wknd-count))
|
||||||
(setq cursor (time-add cursor (days-to-time 1))))
|
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||||||
;; If Mon-Fri, it's a work day. Advance work count.
|
(nthcdr 3 (decode-time
|
||||||
(t
|
(time-add cursor (days-to-time 1))))))))
|
||||||
(setq days-worked (1+ days-worked))
|
((<= mins-left-today 0) ;; after hours
|
||||||
;; Move cursor to start of next day
|
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||||||
(setq cursor (time-add cursor (days-to-time 1)))))))
|
(nthcdr 3 (decode-time
|
||||||
|
(time-add cursor (days-to-time 1))))))))
|
||||||
;; Return [End-Time, Weekend-Days-Count]
|
((<= total-work-mins mins-left-today)
|
||||||
|
(setq cursor (time-add cursor (seconds-to-time (* total-work-mins 60))))
|
||||||
|
(setq total-work-mins 0))
|
||||||
|
(t ;; spill to next day
|
||||||
|
(setq total-work-mins (- total-work-mins mins-left-today))
|
||||||
|
(setq cursor (time-add cursor (seconds-to-time (* mins-left-today 60))))))))
|
||||||
(list cursor wknd-count))))
|
(list cursor wknd-count))))
|
||||||
|
|
||||||
;; --- Helper: Find Blocker End Time ---
|
;; ------------------------------------------------------------
|
||||||
|
;; Helper: Find dependency end time
|
||||||
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--get-blocker-end (blocker-str current-pos task-end-map)
|
(defun gortium/internal--get-blocker-end (blocker-str current-pos task-end-map)
|
||||||
(let ((clean (s-trim blocker-str)) (latest-time nil))
|
"Return the latest end time among the dependencies listed in BLOCKER-STR."
|
||||||
|
(let ((clean (s-trim (format "%s" blocker-str)))
|
||||||
|
(latest-time nil))
|
||||||
(cond
|
(cond
|
||||||
((string-match-p "previous-sibling" clean)
|
((string-match-p "previous-sibling" clean)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char current-pos)
|
(goto-char current-pos)
|
||||||
(let ((found nil))
|
(let ((found nil))
|
||||||
(while (and (not found) (org-get-last-sibling))
|
(while (and (not found) (org-get-last-sibling))
|
||||||
(let* ((sid (org-id-get)) (end (when sid (gethash sid task-end-map))))
|
(let* ((sid (org-id-get))
|
||||||
(when end (setq latest-time end) (setq found t)))))))
|
(end (when sid (gethash sid task-end-map))))
|
||||||
|
(when end
|
||||||
|
(setq latest-time end
|
||||||
|
found t)))))))
|
||||||
((string-match-p "parent" clean)
|
((string-match-p "parent" clean)
|
||||||
(save-excursion (goto-char current-pos)
|
(save-excursion
|
||||||
|
(goto-char current-pos)
|
||||||
(when (org-up-heading-safe)
|
(when (org-up-heading-safe)
|
||||||
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
(setq latest-time (gethash (org-id-get) task-end-map)))))
|
||||||
((string-match "ids(\\(.*?\\))" clean)
|
((string-match "ids(\\(.*?\\))" clean)
|
||||||
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
|
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
|
||||||
(let ((tend (gethash (replace-regexp-in-string "[\"']\\|id:" "" tid) task-end-map)))
|
(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)))
|
(when (and tend
|
||||||
|
(or (null latest-time)
|
||||||
|
(time-less-p latest-time tend)))
|
||||||
(setq latest-time tend))))))
|
(setq latest-time tend))))))
|
||||||
latest-time))
|
latest-time))
|
||||||
|
|
||||||
;; --- Helper: Write Properties (Safe) ---
|
;; ------------------------------------------------------------
|
||||||
(defun gortium/internal--update-properties (pos start wknd id end task-end-map)
|
;; Helper: Get earliest timestamp in entry
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
(defun gortium/org--get-anchor-time ()
|
||||||
|
"Return the earliest timestamp in current entry, or nil."
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char pos) ;; POS is a MARKER
|
(org-back-to-heading t)
|
||||||
(if (and wknd (> wknd 0))
|
(let ((limit (save-excursion (outline-next-heading) (point)))
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string wknd))
|
(best nil))
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" nil))
|
(while (re-search-forward org-ts-regexp-both limit t 1)
|
||||||
(org-schedule nil (format-time-string "<%Y-%m-%d %a %H:%M>" start))
|
(let ((ts (org-time-string-to-time (match-string 0))))
|
||||||
(puthash id end task-end-map)))
|
(when (or (null best) (time-less-p ts best))
|
||||||
|
(setq best ts))))
|
||||||
|
best)))
|
||||||
|
|
||||||
(defun gortium/org--skip-weekend (time)
|
;; ------------------------------------------------------------
|
||||||
"Advance TIME to the next Monday morning if it falls on a weekend."
|
;; Helper: Write timestamp range (NO SCHEDULED)
|
||||||
(let* ((decoded (decode-time time))
|
;; ------------------------------------------------------------
|
||||||
(dow (nth 6 decoded)))
|
(defun gortium/internal--update-properties (pos start wknd id end task-end-map &optional fixed original-start)
|
||||||
(cond
|
"Update WEEKEND_DAYS and write the time range at POS.
|
||||||
((= dow 6) ;; Saturday
|
For fixed tasks, ORIGINAL-START is preserved. OLD SCHEDULED/range lines are removed.
|
||||||
(let ((next (time-add time (days-to-time 2))))
|
The range is inserted directly below the heading, before any drawers."
|
||||||
(apply #'encode-time (append '(0 0 8) (nthcdr 3 (decode-time next))))))
|
|
||||||
((= dow 0) ;; Sunday
|
|
||||||
(let ((next (time-add time (days-to-time 1))))
|
|
||||||
(apply #'encode-time (append '(0 0 8) (nthcdr 3 (decode-time next))))))
|
|
||||||
(t time))))
|
|
||||||
|
|
||||||
;; --- MAIN FUNCTION ---
|
|
||||||
(defun gortium/org-schedule-subtree-chains ()
|
|
||||||
"Schedule tasks using MARKERS and WALKER logic."
|
|
||||||
(interactive)
|
|
||||||
(save-excursion
|
|
||||||
(message "--- Starting Gantt Scheduler ---")
|
|
||||||
(let* ((all-tasks '()) (task-end-times (make-hash-table :test 'equal)))
|
|
||||||
|
|
||||||
;; Collection
|
|
||||||
(org-map-entries
|
|
||||||
(lambda ()
|
|
||||||
(when (org-get-todo-state) ;; Strict TODO check
|
|
||||||
(let ((effort (org-entry-get (point) "EFFORT" nil))
|
|
||||||
(blocker (org-entry-get (point) "BLOCKER" nil))
|
|
||||||
(fixed (org-entry-get (point) "FIXED" nil))
|
|
||||||
(scheduled (org-entry-get (point) "SCHEDULED")))
|
|
||||||
(when (or effort blocker fixed)
|
|
||||||
(push (list (point-marker) (org-id-get-create) (org-get-heading t t t t)
|
|
||||||
effort (when blocker (s-trim blocker)) fixed scheduled
|
|
||||||
(org-entry-get (point) "OFFSET_DAYS" nil)) all-tasks)))))
|
|
||||||
nil (if (org-at-heading-p) 'tree nil))
|
|
||||||
(setq all-tasks (nreverse all-tasks))
|
|
||||||
|
|
||||||
;; Pass 1: FIXED
|
|
||||||
(dolist (task all-tasks)
|
|
||||||
(pcase-let ((`(,pos ,id ,heading ,effort ,blocker ,fixed ,scheduled ,offset) task))
|
|
||||||
(when (and fixed (member (downcase fixed) '("t" "true" "yes")))
|
|
||||||
(if scheduled
|
|
||||||
(let* ((start (org-time-string-to-time scheduled))
|
|
||||||
(span (gortium/internal--calculate-task-span start effort)))
|
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(goto-char pos)
|
(goto-char pos)
|
||||||
(if (and (cadr span) (> (cadr span) 0))
|
(org-show-entry)
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" (number-to-string (cadr span)))
|
|
||||||
(org-entry-put (point) "WEEKEND_DAYS" nil))
|
|
||||||
(puthash id (car span) task-end-times)))
|
|
||||||
(message "WARNING: Fixed task '%s' missing date." heading)))))
|
|
||||||
|
|
||||||
;; Pass 2: CHAINS
|
;; Update WEEKEND_DAYS property
|
||||||
(let* ((remaining (cl-remove-if (lambda (tk) (or (nth 5 tk) (not (nth 4 tk)))) all-tasks))
|
(if (and wknd (> wknd 0))
|
||||||
(iter 0) (limit (* 5 (length all-tasks))))
|
(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))))))
|
||||||
|
|
||||||
|
;; 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"))
|
||||||
|
|
||||||
|
;; Update task-end-map
|
||||||
|
(puthash id end task-end-map)))
|
||||||
|
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
;; MAIN FUNCTION
|
||||||
|
;; ------------------------------------------------------------
|
||||||
|
(defun gortium/org-schedule-subtree-chains ()
|
||||||
|
(interactive)
|
||||||
|
(save-excursion
|
||||||
|
(message "--- Starting Global Gantt Scheduler ---")
|
||||||
|
(let ((all-tasks '())
|
||||||
|
(task-end-times (make-hash-table :test 'equal))
|
||||||
|
(task-end-for-blockers (make-hash-table :test 'equal)))
|
||||||
|
;; COLLECTION
|
||||||
|
(org-map-entries
|
||||||
|
(lambda ()
|
||||||
|
(let ((state (org-get-todo-state)))
|
||||||
|
(when 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))))))
|
||||||
|
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)))
|
||||||
|
(iter 0))
|
||||||
(while (and remaining (< iter limit))
|
(while (and remaining (< iter limit))
|
||||||
(cl-incf iter)
|
(cl-incf iter)
|
||||||
(let ((scheduled-this-loop '()))
|
(let ((done '()))
|
||||||
(dolist (task remaining)
|
(dolist (task remaining)
|
||||||
(pcase-let ((`(,pos ,id ,heading ,effort ,blocker ,fixed ,scheduled ,offset) task))
|
(pcase-let ((`(,pos ,id ,effort ,blocker ,_ ,_ ,offset ,state ,closed) task))
|
||||||
(let ((dep-end (gortium/internal--get-blocker-end blocker pos task-end-times)))
|
;; Determine dependency end
|
||||||
(when dep-end
|
(let* ((dep (or (gortium/internal--get-blocker-end blocker pos
|
||||||
(let* ((off-val (if offset (string-to-number offset) 0))
|
task-end-times)
|
||||||
(base-start (time-add dep-end (days-to-time off-val)))
|
(and (string= state "DONE")
|
||||||
(start (gortium/internal--snap-to-working-hours base-start))
|
(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)))
|
(span (gortium/internal--calculate-task-span start effort)))
|
||||||
(gortium/internal--update-properties pos start (cadr span) id (car span) task-end-times)
|
;; FIXED tasks: keep start
|
||||||
(push task scheduled-this-loop))))))
|
(if (and (not (string= state "DONE")) (org-entry-get pos "FIXED"))
|
||||||
(setq remaining (cl-remove-if (lambda (tk) (member tk scheduled-this-loop)) remaining))))
|
(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)))))
|
||||||
|
|
||||||
(dolist (task all-tasks) (set-marker (car task) nil)) ;; Clean markers
|
(message "--- Scheduler Finished ---"))))
|
||||||
(message "--- Scheduler Finished ---")))))
|
|
||||||
|
|
||||||
;; USAGE:
|
;; ---------------------------------------------
|
||||||
;; 1. Set up your tasks with either:
|
(defun gortium/org-ensure-task-properties ()
|
||||||
;; - :FIXED: t and a SCHEDULED date, OR
|
"Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)
|
||||||
;; - :BLOCKER: previous-sibling / ids(UUID) / ids("id:UUID") / parent
|
and ensure the standard property drawer exists without overwriting existing data."
|
||||||
;; 2. Put cursor on "Planning" heading
|
(interactive)
|
||||||
;; 3. M-x gortium/org-schedule-subtree-chains
|
(save-excursion
|
||||||
;;
|
(message "--- Initializing Task Properties for all states ---")
|
||||||
;; The function will:
|
(let ((count 0)
|
||||||
;; - Use FIXED tasks as anchors
|
;; List of properties to ensure exist
|
||||||
;; - Calculate all other tasks from their dependencies
|
(props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS"
|
||||||
;; - Warn about tasks without BLOCKER or FIXED
|
"ASSIGNEE" "RESOURCES" "CATEGORY"
|
||||||
;; - Detect circular dependencies
|
"DIMENTIONS" "WEIGHT")))
|
||||||
;; - Respect 8-hour workday limits
|
(org-map-entries
|
||||||
;; - Skip weekends
|
(lambda ()
|
||||||
;;
|
;; This check returns true if the heading has ANY todo keyword
|
||||||
;; Supported BLOCKER types (standard EDNA format):
|
(let ((todo-state (org-get-todo-state)))
|
||||||
;; - previous-sibling
|
(when todo-state
|
||||||
;; - ids(UUID) - e.g., ids(70d8f844-1952-43f0-8043-da51e8c8bc69)
|
(cl-incf count)
|
||||||
;; - ids("id:UUID") - e.g., ids("id:70d8f844-1952-43f0-8043-da51e8c8bc69")
|
;; 1. Handle ID (builtin handles 'do not replace' logic)
|
||||||
;; - parent
|
(org-id-get-create)
|
||||||
|
|
||||||
|
;; 2. Ensure all other keys exist
|
||||||
|
(dolist (prop props)
|
||||||
|
;; Only add the property if it's currently nil/missing
|
||||||
|
(unless (org-entry-get (point) prop)
|
||||||
|
(org-entry-put (point) prop
|
||||||
|
(if (string= prop "FIXED") "nil" "")))))))
|
||||||
|
nil nil)
|
||||||
|
(message "--- Finished: Processed %d tasks across all states ---" count))))
|
||||||
|
|
||||||
(defun gortium/add-ids-to-subtree ()
|
(defun gortium/add-ids-to-subtree ()
|
||||||
"Add IDs to all headings in current subtree."
|
"Add IDs to all headings in current subtree."
|
||||||
@@ -1760,20 +1859,6 @@ Otherwise, use SCHEDULED + EFFORT."
|
|||||||
(lambda () (org-id-get-create))
|
(lambda () (org-id-get-create))
|
||||||
nil 'tree)))
|
nil 'tree)))
|
||||||
|
|
||||||
;; Custom function to shift projects
|
|
||||||
(defun gortium/org-shift-subtree-schedules (days)
|
|
||||||
"Shift all SCHEDULED dates in the current subtree by DAYS."
|
|
||||||
(interactive "nDays to shift: ")
|
|
||||||
(org-map-entries
|
|
||||||
(lambda ()
|
|
||||||
(let ((scheduled (org-entry-get nil "SCHEDULED")))
|
|
||||||
(when scheduled
|
|
||||||
(let* ((time (org-time-string-to-time scheduled))
|
|
||||||
(new-time (time-add time (days-to-time days)))
|
|
||||||
(new-date (format-time-string (org-time-stamp-format) new-time)))
|
|
||||||
(org-entry-put nil "SCHEDULED" new-date)))))
|
|
||||||
nil 'tree))
|
|
||||||
|
|
||||||
;; Open link in another frame
|
;; Open link in another frame
|
||||||
(defun gortium/org-open-link-in-other-frame ()
|
(defun gortium/org-open-link-in-other-frame ()
|
||||||
"Open the Org link at point in another frame."
|
"Open the Org link at point in another frame."
|
||||||
|
|||||||
Reference in New Issue
Block a user