Files
dotfiles/doom/.config/doom/config.el
2025-09-20 14:00:35 -04:00

1492 lines
58 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
(setq user-full-name "Thierry Pouplier"
user-mail-address "tpouplier@tdnde.com")
(setq doom-font (font-spec :family "JetBrainsMono Nerd Font" :size 16)
doom-symbol-font (font-spec :family "JetBrainsMono Nerd Font" :size 20))
;; BIGGER FONT MODE
(after! doom-big-font-mode
(setq doom-big-font-increment 8))
(setq doom-theme 'doom-gruvbox)
(setq bookmark-save-flag 1)
(setq global-auto-revert-mode 1)
(map! :leader
(:prefix ("w" . "window")
:desc "Minimize window" "O" #'minimize-window))
;; Super- auto -save
(use-package! super-save
:config
(setq super-save-auto-save-when-idle t)
(setq super-save-all-buffers t)
(setq super-save-triggers
'(switch-to-buffer other-window windmove-up windmove-down
windmove-left windmove-right next-buffer previous-buffer
evil-window-delete evil-window-vsplit evil-window-spliti
evil-prev-buffer evil-next-buffer))
(super-save-mode +1))
;; Fixes..
(require 'cl-lib)
(unless (fboundp 'find-if)
(defalias 'find-if #'cl-find-if))
(unless (fboundp 'getf)
(defalias 'getf #'cl-getf))
(setq
org-directory "~/ExoKortex/"
+org-capture-todo-file "~/ExoKortex/2-Areas/Meta-Planning/Core/master_list.org"
+org-capture-notes-file "~/ExoKortex/2-Areas/Meta-Planning/Core/master_list.org"
+org-capture-journal-file "~/ExoKortex/2-Areas/Meta-Planning/Core/master_list.org"
+org-capture-emails-file "~/ExoKortex/2-Areas/Meta-Planning/Core/master_list.org"
;; +org-capture-central-project-todo-file '"refile.org"
;; +org-capture-project-todo-file '"refile.org"
org-agenda-files (directory-files-recursively "~/ExoKortex/" "\\.org$")
)
(after! org
(setq org-refile-targets
'((nil :maxlevel . 6) ;; Current buffer
(org-agenda-files :maxlevel . 6))) ;; All agenda files
)
(after! org
;; Header size
(custom-set-faces!
'(org-document-title :height 2.0 :weight bold)
'(org-level-1 :foreground "#b8bb26" :weight semi-bold :height 1.45) ; green
'(org-level-2 :foreground "#83a598" :weight semi-bold :height 1.35) ; blue
'(org-level-3 :foreground "#d3869b" :weight semi-bold :height 1.25) ; purple
'(org-level-4 :foreground "#8ec07c" :weight semi-bold :height 1.15) ; aqua
'(org-level-5 :foreground "#fabd2f" :weight normal :height 1.10) ; yellow
'(org-level-6 :foreground "#fe8019" :weight normal :height 1.05) ; orange
'(org-level-7 :foreground "#cc241d" :weight normal :height 1.02) ; red
'(org-level-8 :foreground "#a89984" :weight normal :height 1.00)) ; grey-brown
;; Clamp all levels beyond 8 to use org-level-8 face
(defvar gortium/org-level-8-face
'((t :foreground "#a89984" :weight normal :height 1.00)))
;; Définir org-level-9 à org-level-20 avec la même face que org-level-8
(cl-loop for lvl from 9 to 20
do (eval
`(defface ,(intern (format "org-level-%d" lvl))
',gortium/org-level-8-face
,(format "Face pour org heading level %d (cloné du niveau 8)." lvl)
:group 'org-faces)))
;; Ajout des nouvelles faces dans org-level-faces
(setq org-level-faces
(append org-level-faces
(mapcar (lambda (lvl) (intern (format "org-level-%d" lvl)))
(number-sequence 9 20))))
;; Mise à jour du nombre total de niveaux
(setq org-n-level-faces (length org-level-faces))
)
(after! org
;; Automatically clock in when switching to STRT
(defun gortium/org-clock-in-if-starting ()
"Clock in when task is switched to STRT."
(when (and (string= org-state "STRT")
(not (bound-and-true-p org-clock-resolving-clocks-due-to-idleness)))
(org-clock-in)))
;; Automatically clock out when switching to DONE
(defun gortium/org-clock-out-if-not-starting ()
"Clock out if task was in STRT and is being changed to anything else."
(when (and (string= org-last-state "STRT")
(not (string= org-state "STRT")))
(when (org-clocking-p)
(org-clock-out))))
;; Hook both functions into todo state change
(add-hook 'org-after-todo-state-change-hook #'gortium/org-clock-in-if-starting)
(add-hook 'org-after-todo-state-change-hook #'gortium/org-clock-out-if-not-starting)
;; Enable Org Clocking
(setq org-clock-persist 'history) ;; Save clock history across sessions
(add-hook 'org-mode-hook 'org-clock-load) ; Load persisted clocks
(setq org-clock-persist 'history) ; Or 't for full persistence
(setq org-clock-in-resume t) ; Resume clock when restarting the task
;; Save clock data and state changes when exiting Emacs
(setq org-clock-out-remove-zero-time-clocks t)
(setq org-clock-in-resume t) ;; Resume clocking when moving between tasks
;; Always clock out when exiting Emacs
;; (add-hook 'kill-emacs-hook #'(lambda () (when (org-clocking-p) (org-clock-out))))
;; Mode-line clock display
(setq org-clock-mode-line-total 'auto) ;; show today's total
(org-clock-display) ;; activate the mode-line display
(custom-set-faces!
'(org-mode-line-clock
:foreground "gold"
:weight bold))
)
(after! org
(setq org-columns-default-format "%65ITEM %4TODO %3PRIORITY %ASSIGNEE %5EFFORT{:} %5CLOCKSUM %16SCHEDULED %16DEADLINE %10TAGS")
(setq org-global-properties
'(("Effort_ALL" . "0:10 0:30 1:00 2:00 4:00 8:00")
("ASSIGNEE_ALL" . "Thierry Cath Martin Michel Gabriel Silvie George Miguel")))
(setq org-stuck-projects
'("TODO=\"PROJ\"" ("NEXT") nil ""))
(setq org-agenda-exporter-settings '((ps-print-color-p 'nil)))
(setq
org-agenda-follow-mode nil
org-agenda-skip-deadline-if-done nil
org-agenda-skip-scheduled-if-done nil
org-agenda-skip-timestamp-if-done nil
org-journal-enable-agenda-integration t
org-log-done 'time
org-log-into-drawer t
org-log-redeadline 'time
org-log-reschedule 'time
org-todo-repeat-to-state '"LOOP"
org-todo-keywords
'(
(sequence
;; Tasks
"TODO(t)" "STRT(s)" "LOOP(r)" "NEXT(n)" "DELG(g)"
"WAIT(w@/!)" "HOLD(h@/!)" "IDEA(i)"
;; Framework
"EPIC(e)" "AREA(a)" "PROJ(p)"
;; Done / Cancelled
"|" "DONE(d!)" "CNCL(c@/!)")
;; Checkboxes
(sequence "[ ](T)" "[-](S)" "|" "[X](D)")
;; Special indicators
(sequence "[!](F)" "[?](W)" "|" "[i](I)")
;; Binary questions
(sequence "Y/N(M)" "|" "YES(Y)" "NOP(N)")
)
)
;; Not there yet.. Need better integration with org-modern and gruvbox theme
;; org-todo-keyword-faces
;; '(
;; ;; Main task keywords
;; ("TODO" . (:foreground "orange red" :weight bold))
;; ("PROJ" . (:foreground "orchid" :weight bold))
;; ("STRT" . (:foreground "deep sky blue" :weight bold))
;; ("LOOP" . (:foreground "cyan" :weight bold))
;; ("NEXT" . (:foreground "light salmon" :weight bold))
;; ("DELG" . (:foreground "khaki" :weight bold))
;; ("WAIT" . (:foreground "goldenrod" :weight bold))
;; ("HOLD" . (:foreground "light goldenrod" :weight bold))
;; ("IDEA" . (:foreground "medium purple" :weight bold))
;; ;; Done and cancelled
;; ("DONE" . (:foreground "forest green" :weight bold))
;; ("CNCL" . (:foreground "dim gray" :weight bold :slant italic))
;; ;; Checkbox-like states
;; ("[ ]" . (:foreground "orange red" :weight bold))
;; ("[-]" . (:foreground "deep sky blue" :weight bold))
;; ("[X]" . (:foreground "forest green" :weight bold))
;; ;; Special states
;; ("[?]" . (:foreground "goldenrod" :weight bold))
;; ("[i]" . (:foreground "medium purple" :weight bold))
;; ;; Yes/No states
;; ("Y/N" . (:foreground "light steel blue" :weight bold))
;; ("YES" . (:foreground "pale green" :weight bold))
;; ("NOP" . (:foreground "indian red" :weight bold))
;; )
;; %a : org-store-link
;; %i : insert from selection
;; %? : cursor position at the end
;; %u : unactive date
;; %t : ative date
;; %:subject : subject of the message
;; %:from : The full sender string including name and address
;; %:fromname : The display name of the sender
;; %:fromaddress : The email address of the sender
;; %:to, %:toname, %toaddress : Same for the recipient
;; %:date : Date of the message
;; %:date-timestamp : The date of the message as a active org timestamp
;;
;; Original Doom capture:
;; ("p" "Templates for projects")
;; ("pt" "Project-local todo" entry (file+headline +org-capture-project-todo-file "Inbox") "* TODO %?\n %i\n %a" :prepend t)
;; ("pn" "Project-local notes" entry (file+headline +org-capture-project-notes-file "Inbox") "* %U %?\n %i\n %a" :prepend t)
;; ("o" "Centralized templates for projects")
;; ("ot" "Project todo" entry #'+org-capture-central-project-todo-file "* TODO %?\n %i\n %a" :heading "Tasks" :prepend nil)
;; ("on" "Project notes" entry #'+org-capture-central-project-notes-file "* %U %?\n %i\n %a" :heading "Notes" :prepend t)
;; Code to automate task status, but i think some package exist that do a better job
;; (defun org-summary-todo (n-done n-not-done)
;; "Switch entry to DONE when all subentries are done, to TODO otherwise."
;; (let (org-log-done org-log-states) ; turn off logging
;; (org-todo (if (= n-not-done 0) "DONE" "TODO"))))
;; (add-hook 'org-after-todo-statistics-hook 'org-summary-todo)
(setq gtd/started-head "🚀 Started:")
(setq gtd/next-action-head "👉 Next actions:")
(setq gtd/waiting-head "⏳ Waiting on:")
;; (setq gtd/complete-head "Completed items:")
(setq gtd/project-head "‼ Stuck projects:")
(setq gtd/someday-head "💡 Someday/maybe:")
(setq qaa/q "❓ All the Questions:")
(setq qaa/a " All the Answers:")
(setq review/done "🦾 Completed Tasks")
(setq review/unfinished "🔀 Unfinished Scheduled Tasks")
(setq org-agenda-start-day "+0")
(setq org-agenda-custom-commands
'(
("w" . "Work commands")
("p" . "Personal commands")
("wg" "GTD view"
(
(agenda "" ((org-agenda-span 'day)))
(todo "STRT" ((org-agenda-overriding-header gtd/started-head)))
(todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
(todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
(org-agenda-overriding-header gtd/project-head)))
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
)
((org-agenda-tag-filter-preset '("+work")))
)
("pg" "GTD view"
(
(agenda "" ((org-agenda-span 'day)))
(todo "STRT" ((org-agenda-overriding-header gtd/started-head)))
(todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
(todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
(org-agenda-overriding-header gtd/project-head)))
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
)
((org-agenda-tag-filter-preset '("+perso")))
)
("wq" "All the questions.."
(
(todo "[?]" ((org-agenda-overriding-header qaa/q)))
(todo "[i]" ((org-agenda-overriding-header qaa/a)))
)
((org-agenda-tag-filter-preset '("+work")))
)
("pq" "All the questions.."
(
(todo "[?]" ((org-agenda-overriding-header qaa/q)))
(todo "[i]" ((org-agenda-overriding-header qaa/a)))
)
((org-agenda-tag-filter-preset '("+perso")))
)
("wr" "Weekly Review"
((agenda ""
((org-agenda-overriding-header review/done)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'nottodo 'done))
(org-agenda-span 'week)
(org-agenda-start-day nil)))
(agenda ""
((org-agenda-overriding-header review/unfinished)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-span 'week)
(org-agenda-start-day nil)))
)
((org-agenda-tag-filter-preset '("+work")))
)
("pr" "Weekly Review"
((agenda ""
((org-agenda-overriding-header review/done)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'nottodo 'done))
(org-agenda-span 'week)
(org-agenda-start-day nil)))
(agenda ""
((org-agenda-overriding-header review/unfinished)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-span 'week)
(org-agenda-start-day nil)))
)
((org-agenda-tag-filter-preset '("+perso")))
)
("wp" "Planning"
((agenda ""
((org-agenda-span 'month)
(org-agenda-start-day nil)
(org-agenda-overriding-header "📅 Sheduled Tasks"))
)
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled TODOs")
(org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled))
)
)
)
((org-agenda-tag-filter-preset '("+work")))
)
("pp" "Planning"
((agenda ""
((org-agenda-span 'month)
(org-agenda-start-day nil)
(org-agenda-overriding-header "📅 Sheduled Tasks"))
)
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled TODOs")
(org-agenda-skip-function '(org-agenda-skip-entry-if 'scheduled))
)
)
)
((org-agenda-tag-filter-preset '("+perso")))
)
("wP" "THE PLAN"
((agenda ""
((org-agenda-span 'month)
(org-agenda-start-day nil)
(org-agenda-overriding-header "📅 THE PLAN"))
)
)
((org-agenda-tag-filter-preset '("+work")))
)
)
)
)
(after! org
;; Rust code block setting
(setq rustic-babel-display-error-popup nil)
)
(after! org
(setq
;; LaTeX math preview
org-format-latex-options '(:foreground default :background default :scale 2 :html-foreground "Black"
:html-background "Transparent" :html-scale 1.0
:matchers ("begin" "$1" "$" "$$" "\\(" "\\["))
)
)
(after! org
(setq
org-capture-templates
'(
("t" "TODO" entry (file+olp +org-capture-todo-file "Tasks")
"* IDEA %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i" :prepend t)
("c" "Code TODO" entry
(file+olp +org-capture-todo-file "Tasks")
"* TODO %a\n:PROPERTIES:\n:CREATED: %U\n:END:\n":prepend t)
("n" "Note" entry (file+olp +org-capture-notes-file "Notes")
"* %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
("w" "Question" entry (file+olp +org-capture-notes-file "Questions")
"* [?] %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
("j" "Journal" entry (file+olp+datetree +org-capture-journal-file "Journal")
"* %?\n%a\n%i")
("m" "Meeting" entry (file+olp +org-capture-notes-file "Meetings")
"* %? %U :meeting:\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n/Met with: /")
("a" "Appointment" entry (file+olp +org-capture-journal-file "Appointments")
"* %? :appointment:\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i" :time-prompt t)
("e" "Email Workflow")
("ef" "Follow Up" entry (file+olp +org-capture-emails-file "Mails" "Follow Up") "* TODO Follow up with %:fromname on %a\nSCHEDULED:%t\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
("er" "Read Later" entry (file+olp +org-capture-emails-file "Mails" "Read Later") "* TODO Read: %a\nSCHEDULED:%t\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
("ew" "Waiting Response" entry (file+olp +org-capture-emails-file "Mails" "Waiting Response") "* WAIT Waiting response from %:toname on %a\nDEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i" :immediate-finish t)
)
)
)
(setq org-roam-directory (file-truename "~/ExoKortex/")
org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db")
org-attach-id-dir "assets/"
org-roam-dailies-directory "~/ExoKortex/2-Areas/Meta-Planning/Journal/Daily")
(after! org-roam
org-roam-completion-everywhere t
org-roam-db-autosync-mode 1
(require 'org-roam-dailies)
(setq org-roam-dailies-capture-templates
'(("n" "Note" entry "** %<%H:%M> 📝 %?"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:empty-lines 1)
("t" "Task" entry "** %<%H:%M> 🛠 %a"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:immediate-finish t
:empty-lines 1)
("m" "Meeting" entry "** %<%H:%M> 👥 Meeting %?"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:empty-lines 1)
("u" "Muffin" entry "** %<%H:%M> 🥐 Muffin"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:immediate-finish t
:empty-lines 1)
("e" "Event" entry "** %<%H:%M> 📅 Event: %?"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:empty-lines 1)
("c" "Clock In" entry "** %<%H:%M> ⏱ Clocked In"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:immediate-finish t
:empty-lines 1)
("o" "Clock Out" entry "** %<%H:%M> 🚪 Clocked Out"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n* Journal\n"
("Journal"))
:immediate-finish t
:empty-lines 1)
)
)
)
;; Org-roam-dailies new entry help function
;; Adapted from https://rostre.bearblog.dev/building-my-ideal-emacs-journal/
(defun gortium/org-roam-dailies-new-entry ()
"Create a new entry in org-roam daily notes with interactive prompt.
Handles org-clock and context link capture for tasks."
(interactive)
(let* ((choices '(("Task 🛠" . task)
("Note 📝" . note)
("Meeting 👥" . meeting)
("Muffin 🥐" . muffin)
("Event 📅" . event)
("Clock In ⏱" . in)
("Clock Out 🚪" . out)))
(choice-label (completing-read "Entry type: " (mapcar #'car choices)))
(entry-type (cdr (assoc choice-label choices))))
;; Handle preconditions
(cond
((eq entry-type 'task)
(let ((check-and-clock-in
(lambda ()
(unless (org-entry-get nil "TODO" t)
(user-error "Point is not under a TODO heading"))
(org-todo "STRT")
(org-clock-in)
(org-store-link nil t))))
(cond
((eq major-mode 'org-agenda-mode)
(save-window-excursion
(org-agenda-switch-to)
(funcall check-and-clock-in)))
((eq major-mode 'org-mode)
(funcall check-and-clock-in))
(t
(user-error "Unsupported major mode for task entry")))))
((eq entry-type 'muffin)
(org-clock-out))
((eq entry-type 'out)
(org-clock-out))
((eq entry-type 'in)
(org-clock-in)))
;; Launch capture
(let ((key (pcase entry-type
('note "n") ('task "t") ('meeting "m")
('muffin "m") ('event "e")
('in "c") ('out "o"))))
(org-roam-dailies-capture-date nil nil key))))
;; Connecteam org-roam-dailies integration
(require 'request)
(defvar gortium/connecteam-api-key (auth-source-passage-get 'secret "connecteam") "Your Connecteam API Key")
(defvar gortium/connecteam-user-id "9885891" "Your Connecteam User ID")
(defvar gortium/connecteam-clock-id "9335145" "Your Connecteam time clock ID used in API calls.")
(defun gortium/connecteam-clock-in (job-id time-clock-id)
"Send a clock-in request to Connecteam API for JOB-ID and TIME-CLOCK-ID."
(request
(format "https://api.connecteam.com/time-clock/v1/time-clocks/%s/clock-in" time-clock-id)
:type "POST"
:headers `(("X-API-KEY" . ,gortium/connecteam-api-key)
("accept" . "application/json")
("content-type" . "application/json"))
:data (json-encode `((userId . ,gortium/connecteam-user-id)
(jobId . ,job-id)))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "Connecteam clock-in successful: %s" (alist-get 'message data))))
:error (cl-function
(lambda (&rest args &key error-thrown &allow-other-keys)
(message "Connecteam clock-in failed: %S" error-thrown)))))
(defun gortium/connecteam-clock-out (time-clock-id)
"Send a clock-out request to Connecteam API for TIME-CLOCK-ID."
(request
(format "https://api.connecteam.com/time-clock/v1/time-clocks/%s/clock-out" time-clock-id)
:type "POST"
:headers `(("X-API-KEY" . ,gortium/connecteam-api-key)
("accept" . "application/json")
("content-type" . "application/json"))
:data (json-encode `((userId . ,gortium/connecteam-user-id)))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "Connecteam clock-out successful: %s" (alist-get 'message data))))
:error (cl-function
(lambda (&rest args &key error-thrown &allow-other-keys)
(message "Connecteam clock-out failed: %S" error-thrown)))))
(defun gortium/org-clock-in-to-connecteam ()
"Clock in to Connecteam using the job ID in the current Org task."
(when (org-entry-get nil "CONNECTEAM_JOB_ID" t)
(let ((job-id (org-entry-get nil "CONNECTEAM_JOB_ID" t)))
(gortium/connecteam-clock-in job-id gortium/connecteam-clock-id))))
(add-hook 'org-clock-in-hook #'gortium/org-clock-in-to-connecteam)
(defun gortium/org-clock-out-from-connecteam ()
"Clock out from Connecteam or trigger vacation request if MUFFIN property is set."
(let ((muffin (org-entry-get nil "MUFFIN")))
(if muffin
(gortium/connecteam-vacation-request)
(gortium/connecteam-clock-out gortium/connecteam-clock-id))))
(add-hook 'org-clock-out-hook #'gortium/org-clock-out-from-connecteam)
(after! calendar
;; Define a function to open org-roam daily note for selected date in calendar
(defun gortium/org-roam-dailies-open-at-point ()
"Open org-roam daily note for date at point in calendar."
(interactive)
(let ((date (calendar-cursor-to-date)))
(org-roam-dailies-goto-date date)))
)
;; Bind RET in calendar mode to open daily note instead of default exit
(evil-define-key 'normal calendar-mode-map
(kbd "RET") #'gortium/org-roam-dailies-open-at-point)
(after! dap-mode
(require 'dap-python)
(setq dap-python-debugger 'debugpy)
(dap-ui-mode 1)
(add-hook 'dap-terminated-hook
(lambda ()
(dap-ui-controls-mode -1)
(dap-ui-mode -1))))
(after! dap-python
(dap-register-debug-template
"Python :: My Script with Args"
(list :type "python"
:args ["--start" "73"
"--input" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/Resources/G05730212_111 scan plans/curve_G05730212_111.src"
"--template" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/exit_strat/ExitPath_Template.src"
"--output" "/home/tpouplier/ExoKortex/1-Projects/Exit_strat/exit_strat/output/ExitPath.src"]
:cwd nil
:module nil
:program nil
:request "launch"
:name "Python :: My Script with Args")))
;; LSP BABY
(after! lsp-mode
(setq lsp-csharp-server-path "~/.local/tools/omnisharp-mono/omnisharp.sh")
)
;; To keep my eye in the center of the screen while scrolling. Like in my nvim x)
(setq scroll-margin 10)
;; Enable visual line wrapping globally
;; (global-visual-line-mode 1)
;; Enable visual line wrapping in certain modes
(after! org
(add-hook 'org-mode-hook #'visual-line-mode))
(after! prog-mode
(add-hook 'prog-mode-hook #'visual-line-mode))
(after! text-mode
(add-hook 'text-mode-hook #'visual-line-mode))
(use-package! org-phscroll
:after org)
;; Now I can write x) (spellchecking)
(after! ispell
(setq ispell-program-name "hunspell"
ispell-dictionary "en_CA,fr_CA"
ispell-personal-dictionary "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/perso.dic")
;; Set the dictionary path environment variable
(setenv "DICPATH" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries")
(setq ispell-hunspell-dict-paths-alist
'(("fr_CA" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/fr_CA.aff")
("en_CA" "/home/tpouplier/ExoKortex/2-Areas/IT/Dictionaries/en_CA.aff")))
(setq ispell-hunspell-dictionary-alist
'(("fr_CA" "[[:alpha:]]" "[^[:alpha:]]" "[']" nil ("-d" "fr_CA") nil utf-8)
("en_CA" "[[:alpha:]]" "[^[:alpha:]]" "[']" nil ("-d" "en_CA") nil utf-8)))
;; ispell-set-spellchecker-params has to be called
;; before ispell-hunspell-add-multi-dic will work
(ispell-set-spellchecker-params)
(ispell-hunspell-add-multi-dic "en_CA,fr_CA"))
;; Let me write like a broken engineer, thank you.
(add-hook 'writegood-mode-hook 'writegood-passive-voice-turn-off)
;; Move selected region up or down
(use-package! drag-stuff
:config
;; Enable drag-stuff-mode globally
(drag-stuff-global-mode 1)
;; Set keybindings
(map! :v "M-j" #'drag-stuff-down
:v "M-k" #'drag-stuff-up
:n "M-j" #'drag-stuff-down
:n "M-k" #'drag-stuff-up)
;; Fold bindings
(after! evil
(define-key evil-motion-state-map (kbd "z C") #'hs-hide-level))
;; Disable global bindings for C-j and C-k
(global-unset-key (kbd "C-j"))
(global-unset-key (kbd "C-k"))
;; Optionally define default keybindings (if needed)
(drag-stuff-define-keys))
(setq display-line-numbers-type 'relative)
;; JK to escape was not working. Added it back.
(use-package! evil-escape
:config
(setq evil-escape-excluded-states '(normal visual multiedit emacs motion)
evil-escape-excluded-major-modes '(neotree-mode treemacs-mode vterm-mode)
evil-escape-key-sequence "jk"
evil-escape-delay 0.15)
(evil-escape-mode 1)
)
(after! nix-mode
(setq lsp-nix-nil-formatter ["nixpkgs-fmt"])
(add-hook 'nix-mode-hook #'lsp!))
(after! lsp-mode
(setq lsp-nix-server 'nil))
;; KRL mode
(add-hook 'krl-mode-hook 'font-lock-mode)
(add-hook 'krl-mode-hook 'display-line-numbers-mode)
(use-package! krl-mode
:mode (("\\.src\\'" . krl-mode)
("\\.dat\\'" . krl-mode)
("\\.sub\\'" . krl-mode)))
(use-package! auto-highlight-symbol
:hook (krl-mode . auto-highlight-symbol-mode)
:config
(setq ahs-idle-interval 0.5 ; highlight after 0.5s
ahs-default-range 'ahs-range-whole-buffer ; highlight in whole buffer
ahs-case-fold-search t ; case-INsensitive matching
ahs-include-definition t)) ; highlight definition too
(defcustom krl-formatter-command "python"
"Command to run the KRL formatter."
:type 'string
:group 'krl)
(defcustom krl-formatter-script-path "~/ExoKortex/1-Projects/Exit_strat/exit_strat/scripts/krl_formatter.py"
"Path to the KRL formatter script."
:type 'string
:group 'krl)
(defun krl-format-buffer ()
"Format the current buffer using the KRL formatter."
(interactive)
(let ((original-point (point))
(original-line (line-number-at-pos)))
(shell-command-on-region
(point-min)
(point-max)
(concat krl-formatter-command " " krl-formatter-script-path)
t t)
;; Try to restore cursor position
(goto-char (point-min))
(forward-line (1- original-line))
(message "KRL buffer formatted")))
(defun krl-format-region (start end)
"Format the selected region using the KRL formatter."
(interactive "r")
(shell-command-on-region
start
end
(concat krl-formatter-command " " krl-formatter-script-path)
t t)
(message "KRL region formatted"))
;; Auto-format on save (optional)
(defun krl-format-before-save ()
"Format KRL code before saving."
(when (eq major-mode 'krl-mode)
(krl-format-buffer)))
;; Uncomment the next line to enable auto-formatting on save
(add-hook 'before-save-hook 'krl-format-before-save)
(use-package! hledger-mode
:config
(setq hledger-jfile "~/2-Areas/Finances/finance_vault/tpouplier.hledger")
(setq hledger-top-asset-account "Assets")
(setq hledger-top-income-account "Incomes")
(setq hledger-top-expense-account "Expenses")
(setq hledger-ratios-debt-accounts "Liabilities")
(setq hledger-ratios-assets-accounts "Assets")
(setq hledger-ratios-income-accounts "Incomes")
(setq hledger-ratios-liquid-asset-accounts "Assets:Cash")
(setq hledger-ratios--accounts "Assets:Cash")
;; (setq hledger-ratios-debt-accounts "Liabilities")
(add-to-list 'auto-mode-alist '("\\.hledger\\'" . hledger-mode))
)
;; Hook fragtog to org-mode to have editable latex preview
(use-package! org-fragtog
:hook (org-mode . org-fragtog-mode))
;; Rescale latex preview when changing font size
(defun gortium/org-latex-refresh-on-zoom (&rest _)
"Dynamically rescale and refresh all LaTeX previews after text zoom."
(when (derived-mode-p 'org-mode)
;; Dynamically update scale relative to zoom level
(setq org-format-latex-options
(plist-put org-format-latex-options
:scale (+ 2 (* 0.5 text-scale-mode-amount))
))
;; Clear all previews
(org-clear-latex-preview)
;; Re-render all previews
(org-latex-preview '(16))
))
(advice-add 'text-scale-increase :after #'gortium/org-latex-refresh-on-zoom)
(advice-add 'text-scale-decrease :after #'gortium/org-latex-refresh-on-zoom)
(advice-add 'text-scale-set :after #'gortium/org-latex-refresh-on-zoom)
(use-package! age
:demand t
:config
(setq age-program "rage")
(setq age-default-identity "~/.ssh/gortium_ssh_key")
(setq age-default-recipient "~/.ssh/gortium_ssh_key.pub")
(age-file-enable))
(require 'notifications)
(require 'cl-lib)
(defun gortium/age-notify (msg &optional simple)
"Notify about AGE operations. SIMPLE uses `message` instead of desktop notification."
(if simple
(message "%s" msg)
(if (eq system-type 'gnu/linux)
(notifications-notify
:title "age.el"
:body msg
:urgency 'low
:timeout 800)
(message "%s" msg))))
(defun gortium/age-notify-decrypt (&rest args)
"Notification hook for age decryption."
(cl-destructuring-bind (context cipher) args
(gortium/age-notify (format "Decrypting %s" (age-data-file cipher)) t)))
(defun gortium/age-notify-encrypt (&rest args)
"Notification hook for age encryption."
(cl-destructuring-bind (context plain recipients) args
(gortium/age-notify (format "Encrypting %s" (age-data-file plain)) t)))
(defun gortium/age-toggle-decrypt-notifications ()
"Toggle notifications for age decryption."
(interactive)
(if (advice-member-p #'gortium/age-notify-decrypt #'age-start-decrypt)
(progn
(advice-remove #'age-start-decrypt #'gortium/age-notify-decrypt)
(message "Disabled age decrypt notifications."))
(advice-add #'age-start-decrypt :before #'gortium/age-notify-decrypt)
(message "Enabled age decrypt notifications.")))
(defun gortium/age-toggle-encrypt-notifications ()
"Toggle notifications for age encryption."
(interactive)
(if (advice-member-p #'gortium/age-notify-encrypt #'age-start-encrypt)
(progn
(advice-remove #'age-start-encrypt #'gortium/age-notify-encrypt)
(message "Disabled age encrypt notifications."))
(advice-add #'age-start-encrypt :before #'gortium/age-notify-encrypt)
(message "Enabled age encrypt notifications.")))
;; enable notifications by default
(gortium/age-toggle-decrypt-notifications)
(gortium/age-toggle-encrypt-notifications)
(use-package! passage
:demand t
:config
;; rebind function value for pass to passage
(fset #'pass (lambda () (interactive) (passage)))
(setq age-program "rage")
(setq auth-source-passage-filename (expand-file-name "~/ExoKortex/2-Areas/IT/dotfiles/secrets"))
(setenv "PASSAGE_IDENTITIES_FILE" (expand-file-name age-default-identity))
(setenv "PASSAGE_RECIPIENTS_FILE" (expand-file-name age-default-recipient))
(setenv "PASSAGE_AGE" "rage")
(setenv "PASSAGE_DIR" (expand-file-name "~/ExoKortex/2-Areas/IT/dotfiles/secrets"))
)
;; TUI tools in emacs
(after! eee
(setq ee-terminal-command "kitty")
)
;; Ensure C-j works properly in insert state in vterm
(after! vterm
(add-hook 'vterm-mode-hook
(lambda ()
(evil-local-set-key 'insert (kbd "C-j") #'vterm--self-insert))))
;; GPTel AI chat for emacs
(use-package! gptel
:config
;; (advice-add 'gptel-rewrite :after #'gptel-rewrite-diff)
(add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
(setq gptel-display-buffer-action
'(display-buffer-same-window))
(defun gptel-set-default-directory ()
(unless (buffer-file-name)
(setq default-directory "~/ExoKortex/3-Resources/AI-Chat/")))
(add-hook 'gptel-mode-hook #'gptel-set-default-directory)
(setq gptel-expert-commands t
gptel-default-mode 'org-mode
;; gptel-model 'OpenRouter:deepseek/deepseek-chat-v3-0324:free
gptel-api-key (auth-source-passage-get 'secret "openrouter"))
(require 'gptel-integrations)
(gptel-make-openai "OpenRouter"
:host "openrouter.ai"
:endpoint "/api/v1/chat/completions"
:stream t
:key (auth-source-passage-get 'secret "openrouter")
:models '(deepseek/deepseek-r1-0528-qwen3-8b:free
google/gemini-2.0-flash-exp:free
deepseek/deepseek-chat-v3-0324:free
meta-llama/llama-4-maverick:free
qwen/qwen3-coder:free))
(gptel-make-gemini "Gemini"
:key (auth-source-passage-get 'secret "gemini")
:stream t
:models '(gemini-2.5-pro
gemini-2.5-flash))
(gptel-make-ollama "Ollama" ;Any name of your choosing
:host "localhost:11434" ;Where it's running
:stream t ;Stream responses
:models '(deepseek-r1:1.5b
orieg/gemma3-tools:4b)) ;List of models
(gptel-make-openai "OpenWebUI"
:host "ai.aziworkhorse.duckdns.org"
:curl-args '("--insecure") ; needed for self-signed certs
:key (auth-source-passage-get 'secret "openwebui")
:endpoint "/api/chat/completions"
:stream t
:models '("orieg/gemma3-tools:1b"))
)
(map! :after gptel
:leader
(:prefix ("r" . "GPTel Rewrite")
:desc "Rewrite region" "r" #'gptel-rewrite
:desc "Show rewrite diff" "d" #'gptel--rewrite-diff
:desc "Accept rewrite" "a" #'gptel--rewrite-accept
:desc "Reject rewrite" "x" #'gptel--rewrite-reject
:desc "Iterate rewrite" "i" #'gptel--rewrite-iterate))
(diff-hl-mode +1)
;; set `tramp-direct-async-process' locally in all ssh connections
(connection-local-set-profile-variables
'remote-direct-async-process
'((tramp-direct-async-process . t)))
(connection-local-set-profiles
'(:application tramp :protocol "ssh")
'remote-direct-async-process)
;; Dirvish config
(after! dirvish
;; Display icons, file size, timestamps, etc.
(setq dirvish-attributes
'(all-the-icons subtree-state file-size file-time))
(setq dirvish-default-layout '(0.3 0.15 0.55))
;; Use a header line instead of the traditional dired modeline
(setq dirvish-use-header-line t)
;; Show dotfile please, ty
;; (setq dired-omit-mode nil)
;; (setq dired-listing-switches "-a")
;; Replace default dired mode globally
(dirvish-override-dired-mode)
(require 'dirvish-yank)
(setq! dirvish-quick-access-entries
`(
("h" "~/" "Home")
("k" "~/ExoKortex/2-Areas/Meta-Planning/Core" "ExoKortex")
("p" "~/ExoKortex/1-Projects" "Projects")
("a" "~/ExoKortex/2-Areas" "Areas")
("r" "~/ExoKortex/3-Resources" "Resources")
("i" "~/ExoKortex/4-Archives" "Archives")
("d" "~/Downloads/" "Downloads")
("u" "/run/media" "Mounted drives")
("t" "~/.local/share/Trash/files/" "Trash")
)))
;; Enable previewing of surrounding lines in consult-ripgrep
(setq consult-ripgrep-preview t)
;; Weather
(after! wttrin
(setq wttrin-default-cities '("Blainville" "Canada"))
)
;; Not working..
;; (setq weather-metno-location-name "Blainville, Canada"
;; weather-metno-location-latitude 45
;; weather-metno-location-longitude 73)
;; Modern look for org
(use-package! org-modern
:after org
:config
(global-org-modern-mode)
(setq org-modern-todo-faces
'(
;; Framework
("EPIC" :foreground "#b16286" :inverse-video t :weight bold) ; large project
("AREA" :foreground "#83a598" :inverse-video t :weight bold) ; domain
("PROJ" :foreground "#458588" :inverse-video t :weight bold) ; specific project
;; Tasks
("TODO" :foreground "#fb4934") ; to do
("STRT" :foreground "#fe8019" :weight bold :inverse-video t) ; started
("NEXT" :foreground "#b8bb26" :weight bold :underline t) ; next action
("LOOP" :foreground "#fabd2f") ; recurring
("DELG" :foreground "#8ec07c") ; delegated
("WAIT" :foreground "#fabd2f" :slant italic) ; waiting
("HOLD" :foreground "#7c6f64" :slant italic) ; on hold
("IDEA" :foreground "#d65d0e" :slant italic) ; idea
;; Done / Cancelled
("DONE" :foreground "#98971a") ; done
("CNCL" :foreground "#928374" :strike-through t) ; cancelled
;; Checkboxes
("[ ]" :foreground "#fb4934") ; unchecked
("[-]" :foreground "#fabd2f") ; partial
("[X]" :foreground "#98971a") ; checked
;; Special indicators
("[!]" :foreground "#fb4934" :weight bold :inverse-video t) ; urgent
("[?]" :foreground "#d79921" :weight bold) ; uncertain
("[i]" :foreground "#b16286" :slant italic) ; info
;; Binary questions
("Y/N" :foreground "#fe8019" :weight bold) ; question
("YES" :foreground "#b8bb26") ; yes
("NOP" :foreground "#928374") ; no
)
)
)
;; Flash the point (cursor) when moving between window
(use-package! beacon
:config
(beacon-mode 1))
;; Deleted file go to trash instead of been destroyed for ever... (rm -r / --do-it)
(setq delete-by-moving-to-trash t
trash-directory "~/.local/share/Trash/files/")
(after! org-msg
(setq mail-user-agent 'mu4e-user-agent)
(require 'org-msg)
(setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t"
org-msg-startup "hidestars indent inlineimages"
org-msg-greeting-fmt "\nBonjour%s,\n\n"
;; org-msg-recipient-names '(("jeremy.compostella@gmail.com" . "Jérémy"))
org-msg-greeting-name-limit 1
org-msg-default-alternatives '((new . (text html))
(reply-to-html . (text html))
(reply-to-text . (text)))
org-msg-convert-citation t
org-msg-signature "
Cdlt,
#+begin_signature
--
*Thierry Pouplier*
1319 rue Bergar, Laval, Qc, Canada \\\\
H7L 4Z7 \\\\
Tél. : +1 (450) 667-1884 \\\\
Cell. : +1 (514) 887-1674 \\\\
tpouplier@tdnde.com \\\\
www.tdnde.com \\\\
#+end_signature")
)
(after! mu4e
;; Each path is relative to the path of the maildir you passed to mu
(set-email-account! "tpouplier@tdnde.com"
'(
(mu4e-change-filenames-when-moving t)
(mu4e-update-interval 120)
(mu4e-sent-folder . "/Sent")
(mu4e-drafts-folder . "/Drafts")
(mu4e-trash-folder . "/Trash")
(mu4e-refile-folder . "/Inbox")
(smtpmail-smtp-user . "tpouplier@tdnde.com")
(smtpmail-stream-type . ssl)
(smtpmail-smtp-server . "smtp.hostinger.com")
(smtpmail-smtp-service . 465)
;; (mu4e-maildir-shortcuts
;; '(
;; ("Inbox" . ?i)
;; ("Drafts" . ?d)
;; ("Sent" . ?s)
;; ("Trash" . ?t)
;; ))
)
t)
)
(after! circe
;; set your nick, username, and real name
(setq circe-nick "gortium"
circe-user-name "gortium"
circe-real-name "gortium")
(setq circe-network-options
'(("Libera Chat"
:tls t
:nick "gortium"
:sasl-username "gortium"
:sasl-password (auth-source-passage-get 'secret "irc")
:channels ("#emacs-circe")
)))
)
;; Enable midnight mode by default for PDF files
(after! pdf-tools
(add-hook 'pdf-view-mode-hook (lambda () (pdf-view-midnight-minor-mode 1))))
;; Like Emacs everywhere, but work in hyprland
(defun thanos/wtype-text (text)
"Process TEXT for wtype, handling newlines properly."
(let* ((has-final-newline (string-match-p "\n$" text))
(lines (split-string text "\n"))
(last-idx (1- (length lines))))
(string-join
(cl-loop for line in lines
for i from 0
collect (cond
;; Last line without final newline
((and (= i last-idx) (not has-final-newline))
(format "wtype -s 350 \"%s\""
(replace-regexp-in-string "\"" "\\\\\"" line)))
;; Any other line
(t
(format "wtype -s 350 \"%s\" && wtype -k Return"
(replace-regexp-in-string "\"" "\\\\\"" line)))))
" && ")))
(define-minor-mode thanos/type-mode
"Minor mode for inserting text via wtype."
:keymap `((,(kbd "C-c C-c") . ,(lambda () (interactive)
(call-process-shell-command
(thanos/wtype-text (buffer-string))
nil 0)
(delete-frame)))
(,(kbd "C-c C-k") . ,(lambda () (interactive)
(kill-buffer (current-buffer))))))
(defun thanos/type ()
"Launch a temporary frame with a clean buffer for typing."
(interactive)
(let ((frame (make-frame '((name . "emacs-float")
(fullscreen . 0)
(undecorated . t)
(width . 70)
(height . 20))))
(buf (get-buffer-create "emacs-float")))
(select-frame frame)
(switch-to-buffer buf)
(with-current-buffer buf
(erase-buffer)
(org-mode)
(flyspell-mode)
(thanos/type-mode)
(setq-local header-line-format
(format " %s to insert text or %s to cancel."
(propertize "C-c C-c" 'face 'help-key-binding)
(propertize "C-c C-k" 'face 'help-key-binding)))
;; Make the frame more temporary-like
(set-frame-parameter frame 'delete-before-kill-buffer t)
(set-window-dedicated-p (selected-window) t))))
;;Refile in datetree function
(defun org-refile-to-datetree (&optional file)
"Refile a subtree to a datetree corresponding to its timestamp.
The current time is used if the entry has no timestamp.
If FILE is nil, refile in the current file."
(interactive
(list (read-file-name "Refile to file: "
nil ; directory
(buffer-file-name) ; default filename
t))) ; mustmatch
(let* ((datetree-date (or (org-entry-get nil "TIMESTAMP" t)
(org-read-date t nil "now")))
(date (org-date-to-gregorian datetree-date)))
(with-current-buffer (current-buffer)
(save-excursion
(org-cut-subtree)
(when file
(find-file file))
(org-datetree-find-date-create date)
(org-narrow-to-subtree)
(show-subtree)
(org-end-of-subtree t)
(newline)
(goto-char (point-max))
(org-paste-subtree 4)
(widen)))))
;; Open all org fold in ediff
(defun org-ediff-prepare-buffer ()
(when (memq major-mode '(org-mode emacs-lisp-mode))
(outline-show-all)))
(add-hook 'ediff-prepare-buffer-hook 'org-ediff-prepare-buffer)
(defun gortium/add-trigger-scheduling-next ()
"Add scheduled chain for this entry."
(interactive)
(org-set-property "TRIGGER" "next-sibling scheduled!(\"++0d\") todo!(NEXT) chain!(\"TRIGGER\")"))
(defun gortium/org-schedule-after-previous-sibling ()
"Schedule the current task right after its previous sibling.
If the sibling is DONE, use its CLOSED time.
Otherwise, use SCHEDULED + EFFORT."
(interactive)
(unless (org-at-heading-p)
(org-back-to-heading t))
(let ((current-level (org-current-level))
(current-point (point))
prev-end-time)
(save-excursion
;; 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
;; If task is DONE and CLOSED exists
((and (member state org-done-keywords) closed)
(setq prev-end-time (org-time-string-to-time closed)))
;; Else use SCHEDULED + EFFORT
((and scheduled effort)
(let* ((sched-time (org-time-string-to-time scheduled))
(duration-min (org-duration-to-minutes effort)))
(setq prev-end-time
(time-add sched-time (seconds-to-time (* 60 duration-min))))))
(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))))
(defun gortium/org-schedule-siblings-chain ()
"Schedule each sibling task sequentially starting from a selected date.
DONE tasks use their CLOSED time as reference.
Pending tasks are scheduled based on the previous end time (SCHEDULED + EFFORT).
Skips weekends and respects max daily effort starting at `gortium/org-day-start-hour`.
Starting at `gortium/org-day-start-hour` (e.g., 9 means 09:00 AM)."
(interactive)
(let ((gortium/org-day-start-hour 8) ;; Change to choose start of work day
(max-daily-effort-min (* 8 60))) ;; Change to choose the max work hours per day
(let* ((time-str (org-read-date nil nil nil "Start date and time for scheduling:"))
(parsed-time (org-parse-time-string time-str))
(hour (nth 2 parsed-time))
(min (nth 1 parsed-time))
(sec (nth 0 parsed-time))
(final-time (apply #'encode-time
(list (or sec 0)
(or min 0)
;; if hour is nil, use `gortium/org-day-start-hour`
(if hour hour gortium/org-day-start-hour)
(nth 3 parsed-time)
(nth 4 parsed-time)
(nth 5 parsed-time)))))
(save-excursion
(unless (org-at-heading-p)
(org-back-to-heading t))
(let* ((level (org-current-level))
(siblings (list (point)))
(daily-effort 0)
(prev-end (gortium/org--skip-weekend final-time)))
;; Collect sibling positions
(while (outline-get-next-sibling)
(when (= (org-current-level) level)
(push (point) siblings)))
(setq siblings (nreverse siblings))
;; Helpers
(cl-labels
((get-task-end (pos)
(goto-char pos)
(let* ((state (org-get-todo-state))
(closed (org-entry-get nil "CLOSED"))
(scheduled (org-entry-get nil "SCHEDULED"))
(effort (org-entry-get nil "EFFORT")))
(cond
((and (member state org-done-keywords) closed)
(org-time-string-to-time closed))
((and scheduled effort)
(let* ((sched-time (org-time-string-to-time scheduled))
(duration-min (org-duration-to-minutes effort)))
(time-add sched-time (seconds-to-time (* 60 duration-min)))))
(t nil))))
(task-effort-seconds (pos)
(goto-char pos)
(let ((effort (org-entry-get nil "EFFORT")))
(if effort
(* 60 (org-duration-to-minutes effort))
0)))
(next-day-start (time)
"Return next working day's start time at `gortium/org-day-start-hour`, skipping weekends."
(let ((next (time-add time (days-to-time 1))))
(gortium/org--skip-weekend
(apply #'encode-time `(0 0 ,gortium/org-day-start-hour
,(nth 3 (decode-time next))
,(nth 4 (decode-time next))
,(nth 5 (decode-time next)))))))
(schedule-task (pos time)
(goto-char pos)
(org-schedule nil (format-time-string (org-time-stamp-format t) time))))
;; Schedule first task
(setq daily-effort (task-effort-seconds (car siblings)))
(schedule-task (car siblings) prev-end)
(setq prev-end (time-add prev-end (seconds-to-time daily-effort)))
;; Process the rest
(dolist (pos (cdr siblings))
(let ((effort-sec (task-effort-seconds pos)))
(when (> (+ daily-effort effort-sec) (* 60 max-daily-effort-min))
(setq prev-end (next-day-start prev-end))
(setq daily-effort 0))
(schedule-task pos prev-end)
(setq daily-effort (+ daily-effort effort-sec))
(setq prev-end (time-add prev-end (seconds-to-time effort-sec)))))))))))
(defun gortium/org--skip-weekend (time)
"Advance TIME to the next weekday if it's Saturday or Sunday."
(let ((dow (nth 6 (decode-time time))))
(cond
((= dow 6) (time-add time (days-to-time 2))) ; Saturday → Monday
((= dow 0) (time-add time (days-to-time 1))) ; Sunday → Monday
(t time))))
;; 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
(defun gortium/org-open-link-in-other-frame ()
"Open the Org link at point in another frame."
(interactive)
(let ((org-link-frame-setup '((file . find-file-other-frame)
(id . find-file-other-frame))))
(org-open-at-point)))
(map! :after org
:map org-mode-map
:n "gF" #'gortium/org-open-link-in-other-frame)
(defvar my/main-frame nil
"The main Emacs frame where buffers should be toggled to/from.")
(add-hook 'emacs-startup-hook
(lambda () (setq my/main-frame (selected-frame))))
(defun my--rightmost-non-minibuffer-window (frame)
"Return the rightmost non-minibuffer window in FRAME, or nil if none."
(let ((best-win nil)
(best-right -1))
(dolist (w (window-list frame))
(unless (window-minibuffer-p w)
(let ((right (nth 2 (window-edges w))))
(when (> right best-right)
(setq best-right right
best-win w)))))
best-win))
(defun my--non-minibuffer-window-count (frame)
"Return number of non-minibuffer windows in FRAME."
(let ((count 0))
(dolist (w (window-list frame))
(unless (window-minibuffer-p w)
(setq count (1+ count))))
count))
(defun my/toggle-buffer-to-frame ()
"Toggle the current buffer between `my/main-frame' and a new frame."
(interactive)
(unless (frame-live-p my/main-frame)
(setq my/main-frame (selected-frame)))
(let* ((buf (current-buffer))
(old-frame (selected-frame))
(old-win (selected-window)))
(if (eq old-frame my/main-frame)
;; === Move from main frame to new frame ===
(let ((new-frame (make-frame '((name . "Toggled Buffer")))))
(with-selected-frame new-frame
(switch-to-buffer buf))
(select-frame-set-input-focus new-frame)
(with-selected-frame old-frame
(with-selected-window old-win
(if (> (my--non-minibuffer-window-count old-frame) 1)
(delete-window old-win)
(switch-to-buffer (other-buffer buf t))))))
;; === Move from secondary frame back to main ===
(with-selected-frame my/main-frame
(let ((target (my--rightmost-non-minibuffer-window my/main-frame)))
(if target
(with-selected-window target
(condition-case nil
(let ((new-win (split-window-right)))
(select-window new-win)
(switch-to-buffer buf))
(error ;; too small to split, just reuse the window
(switch-to-buffer buf))))
;; fallback: no window found, just use current one
(switch-to-buffer buf))))
;; Close secondary frame or just the window
(with-selected-frame old-frame
(if (= (my--non-minibuffer-window-count old-frame) 1)
(delete-frame old-frame)
(when (window-live-p old-win)
(delete-window old-win)))))))
;; >=== ExoKortex System ===<
(defvar gortium/org-repo "~/ExoKortex/2-Areas/Meta-Planning/Projects/"
"Path to your main Org repository where real .org files are stored.")
(defvar gortium/projects-root "~/ExoKortex/1-Projects/"
"Root directory where new projects will be created.")
(defun gortium/create-project (project-name)
"Create a new project with PROJECT-NAME in `gortium/projects-root`.
The .org file is created in `gortium/org-repo` and symlinked into the project folder."
(interactive "sProject name: ")
(let* ((project-dir (expand-file-name project-name gortium/projects-root))
(org-file (concat project-name ".org"))
(org-real (expand-file-name org-file gortium/org-repo))
(org-link (expand-file-name org-file project-dir)))
(make-directory project-dir t)
(unless (file-exists-p org-real)
(with-temp-file org-real
(insert (format "#+TITLE: %s\n#+DATE: %s\n\n" project-name (format-time-string "%Y-%m-%d")))))
(make-symbolic-link org-real org-link t)
(find-file org-link)))
(defun gortium/create-symlinked-org (file-name)
"Create a symlinked org FILE-NAME in current dir, real file in `gortium/org-repo`."
(interactive "sOrg file name (without .org): ")
(let* ((org-file (concat file-name ".org"))
(org-real (expand-file-name org-file gortium/org-repo))
(org-link (expand-file-name org-file default-directory)))
(unless (file-exists-p org-real)
(with-temp-file org-real
(insert (format "#+TITLE: %s\n#+DATE: %s\n\n" file-name (format-time-string "%Y-%m-%d")))))
(make-symbolic-link org-real org-link t)
(find-file org-link)))
(defun gortium/convert-marked-org-to-symlink ()
"Convert all marked org files in Dirvish/Dired to symlinks in `gortium/org-repo`."
(interactive)
(let ((files (dired-get-marked-files)))
(dolist (file-path files)
(when (and (file-regular-p file-path)
(string= (file-name-extension file-path) "org"))
(let* ((file-name (file-name-nondirectory file-path))
(org-real (expand-file-name file-name gortium/org-repo)))
(unless (file-exists-p org-real)
(rename-file file-path org-real))
(make-symbolic-link org-real file-path t)
(message "Converted %s to symlink -> %s" file-path org-real))))))