2132 lines
86 KiB
EmacsLisp
2132 lines
86 KiB
EmacsLisp
;;; $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))
|
||
|
||
(use-package! ultra-scroll
|
||
:init
|
||
(setq scroll-margin 0 ; Required for ultra-scroll
|
||
scroll-conservatively 101 ; Prevents jumping to center
|
||
ultra-scroll-acceleration nil ; Essential for Miryoku keys
|
||
ultra-scroll-mouse-scale 0.1 ; Lower for higher precision
|
||
pixel-scroll-precision-large-scroll-height 20)
|
||
:config
|
||
(ultra-scroll-mode 1))
|
||
|
||
;; 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/Org/Core/master_list.org"
|
||
+org-capture-notes-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
|
||
+org-capture-journal-file "~/ExoKortex/2-Areas/Meta-Planning/Org/Core/master_list.org"
|
||
+org-capture-emails-file "~/ExoKortex/2-Areas/Meta-Planning/Org/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.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
|
||
'("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-agenda-span 'day
|
||
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
|
||
;; %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 'week)))
|
||
(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 'week)))
|
||
(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" "Installation Bombardier"
|
||
((agenda ""
|
||
((org-agenda-span 60)
|
||
(org-agenda-start-day "2026-01-29")
|
||
(org-agenda-overriding-header "📅 Installation Bombardier")
|
||
(org-agenda-prefix-format "%12t") ;; reserve time space
|
||
(org-agenda-todo-keyword-format " %-12s ") ;; fixed-width TODO
|
||
(org-agenda-tags-column -100) ;; right-align tags
|
||
(org-agenda-time-grid nil)
|
||
)))
|
||
|
||
((org-agenda-tag-filter-preset '("+BA_ON_SITE")))
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
(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! ox-latex
|
||
(setq org-latex-compiler "xelatex"))
|
||
|
||
(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 +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)
|
||
)
|
||
)
|
||
)
|
||
|
||
;; Enable plantuml-mode for PlantUML files
|
||
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
|
||
|
||
;; (require 'cl-lib)
|
||
;; (require 'dash)
|
||
;; (unless (fboundp 'first) (defalias 'first #'car))
|
||
|
||
;; ;; Clear rules to ensure the new global color logic takes effect immediately
|
||
;; (setq elgantt--display-rules nil)
|
||
|
||
;; (defface gortium/elgantt-weekend-face
|
||
;; '((t (:background "#32302f" :extend nil)))
|
||
;; "Gruvbox Dark0_Hard/Soft mix for subtle weekend stripes.")
|
||
|
||
;; (defun gortium/internal--month-to-num (name)
|
||
;; "Convert month string to number safely."
|
||
;; (let ((case-fold-search t))
|
||
;; (cond ((string-match-p "Jan" name) 1) ((string-match-p "Feb" name) 2)
|
||
;; ((string-match-p "Mar" name) 3) ((string-match-p "Apr" name) 4)
|
||
;; ((string-match-p "May" name) 5) ((string-match-p "Jun" name) 6)
|
||
;; ((string-match-p "Jul" name) 7) ((string-match-p "Aug" name) 8)
|
||
;; ((string-match-p "Sep" name) 9) ((string-match-p "Oct" name) 10)
|
||
;; ((string-match-p "Nov" name) 11) ((string-match-p "Dec" name) 12) (t 1))))
|
||
|
||
;; (defun gortium/elgantt-draw-weekend-guides ()
|
||
;; "Draw weekend guides for the ENTIRE buffer once to prevent scroll lag."
|
||
;; (interactive)
|
||
;; (when (derived-mode-p 'elgantt-mode)
|
||
;; (let* ((inhibit-modification-hooks t)
|
||
;; (header-line-1 (save-excursion
|
||
;; (goto-char (point-min))
|
||
;; (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
|
||
;; (col-indices '())
|
||
;; (search-pos 0))
|
||
|
||
;; (save-excursion
|
||
;; (save-restriction
|
||
;; (widen)
|
||
;; ;; 1. Clear ALL weekend overlays in the entire buffer
|
||
;; (remove-overlays (point-min) (point-max) 'gortium-weekend t)
|
||
|
||
;; ;; 2. Parse header once to find column indexes (Fast)
|
||
;; (while (string-match "|[[:space:]]*\\([[:alpha:]]+\\)[[:space:]]+\\([0-9]\\{4\\}\\)" header-line-1 search-pos)
|
||
;; (let* ((month-start-col (match-beginning 0))
|
||
;; (month-name (match-string 1 header-line-1))
|
||
;; (year (string-to-number (match-string 2 header-line-1)))
|
||
;; (month-num (gortium/internal--month-to-num month-name))
|
||
;; (next-pipe (string-match "|" header-line-1 (1+ month-start-col)))
|
||
;; (month-width (if next-pipe (- next-pipe month-start-col 1) 31)))
|
||
;; (dotimes (d month-width)
|
||
;; (let* ((day (1+ d))
|
||
;; (time (condition-case nil (encode-time 0 0 12 day month-num year) (error nil))))
|
||
;; (when time
|
||
;; (let ((dow (nth 6 (decode-time time)))
|
||
;; (actual-col (+ month-start-col 1 d)))
|
||
;; (when (member dow '(0 6))
|
||
;; (push actual-col col-indices))))))
|
||
;; (setq search-pos (or next-pipe (length header-line-1)))))
|
||
|
||
;; ;; 3. Apply to the WHOLE buffer line by line
|
||
;; (unless (null col-indices)
|
||
;; (goto-char (point-min))
|
||
;; (forward-line 2) ;; Skip headers
|
||
;; (while (not (eobp))
|
||
;; (let ((line-end (line-end-position)))
|
||
;; (dolist (col col-indices)
|
||
;; (move-to-column col)
|
||
;; (let ((p (point)))
|
||
;; ;; Ensure we are still on the same line and at the correct column
|
||
;; (when (and (< p line-end) (= (current-column) col))
|
||
;; (let ((ov (make-overlay p (1+ p))))
|
||
;; (overlay-put ov 'face 'gortium/elgantt-weekend-face)
|
||
;; (overlay-put ov 'gortium-weekend t)
|
||
;; (overlay-put ov 'priority 100)
|
||
;; (overlay-put ov 'evaporate t))))))
|
||
;; (forward-line 1)))))
|
||
;; (message "Weekend guides rendered for the whole buffer."))))
|
||
|
||
;; ;; Run it only once when the buffer is loaded
|
||
;; (add-hook 'elgantt-mode-hook #'gortium/elgantt-draw-weekend-guides)
|
||
|
||
;; (use-package! elgantt
|
||
;; :commands (elgantt-open elgantt-open-current-org-file)
|
||
;; :config
|
||
;; ;; --- 1. Environment & UI ---
|
||
;; (add-hook 'elgantt-mode-hook
|
||
;; (lambda ()
|
||
;; (setq-local org-phscroll-mode nil)
|
||
;; (setq-local image-roll-mode nil)
|
||
;; (setq truncate-lines t)))
|
||
|
||
;; (setq elgantt-start-date "2026-01-01")
|
||
|
||
;; (setq elgantt-header-column-offset 40
|
||
;; elgantt-header-type 'root
|
||
;; elgantt-show-header-depth t
|
||
;; elgantt-insert-blank-line-between-top-level-header t
|
||
;; elgantt-startup-folded nil
|
||
;; elgantt-draw-overarching-headers nil
|
||
;; elgantt-scroll-to-current-month-at-startup nil)
|
||
|
||
;; (setq elgantt-user-set-color-priority-counter 0)
|
||
|
||
;; (elgantt-create-display-rule draw-active-timestamp-range
|
||
;; :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
|
||
;; (split-string colors " "))))
|
||
;; (range-dates . ((save-excursion
|
||
;; (org-back-to-heading t)
|
||
;; (let ((limit (save-excursion (outline-next-heading) (point))))
|
||
;; (when (re-search-forward "<\\([^>]+\\)>--<\\([^>]+\\)>" limit t)
|
||
;; (list (match-string 1) (match-string 2))))))))
|
||
;; :args (elgantt-org-id)
|
||
;; :body ((when (and elgantt-org-id range-dates)
|
||
;; (let* ((colors (or override-color '("#fabd2f" "#fe8019")))
|
||
;; (s-str (substring (car range-dates) 0 10))
|
||
;; (e-str (substring (cadr range-dates) 0 10))
|
||
;; (p1 (save-excursion (when (elgantt--goto-date s-str) (point))))
|
||
;; (p2 (save-excursion (when (elgantt--goto-date e-str) (point)))))
|
||
;; (when (and (numberp p1) (numberp p2))
|
||
;; (elgantt--draw-gradient
|
||
;; (car colors) (cadr colors)
|
||
;; (truncate p1) (truncate p2) nil ;; <-- FIX: Removed (1+ ...) to stop overshoot
|
||
;; `(priority ,(setq elgantt-user-set-color-priority-counter
|
||
;; (1- elgantt-user-set-color-priority-counter))
|
||
;; :elgantt-user-overlay ,elgantt-org-id))))))))
|
||
|
||
;; ;; --- 2. Effort Rule (With Weekend Extension) ---
|
||
;; ;; (elgantt-create-display-rule draw-scheduled-to-effort-end
|
||
;; ;; :parser ((override-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
|
||
;; ;; (split-string colors " "))))
|
||
;; ;; (elgantt-effort . ((org-entry-get (point) "EFFORT")))
|
||
;; ;; (wknd-days . ((when-let ((val (org-entry-get (point) "WEEKEND_DAYS")))
|
||
;; ;; (string-to-number val)))))
|
||
;; ;; :args (elgantt-scheduled elgantt-effort elgantt-org-id)
|
||
;; ;; :body ((when (and elgantt-scheduled elgantt-effort)
|
||
;; ;; (let* ((start-ts (ts-parse elgantt-scheduled))
|
||
;; ;; (raw-mins (org-duration-to-minutes elgantt-effort))
|
||
;; ;; ;; Add the weekend jump days to the visual length
|
||
;; ;; (total-days (+ (ceiling (/ (float raw-mins) 1440.0)) (or wknd-days 0)))
|
||
;; ;; (p1 (save-excursion
|
||
;; ;; (elgantt--goto-date (ts-format "%Y-%m-%d" start-ts))
|
||
;; ;; (point)))
|
||
;; ;; (colors (or override-color '("#8ec07c" "#458588"))))
|
||
;; ;; (when (numberp p1)
|
||
;; ;; (if (<= total-days 1)
|
||
;; ;; (elgantt--create-overlay (truncate p1) (1+ (truncate p1))
|
||
;; ;; `(face (:background ,(car colors))
|
||
;; ;; priority ,(setq elgantt-user-set-color-priority-counter
|
||
;; ;; (1- elgantt-user-set-color-priority-counter))
|
||
;; ;; :elgantt-user-overlay ,elgantt-org-id))
|
||
;; ;; ;; FIX 1: compute p2 by date (handles "|" separators)
|
||
;; ;; ;; FIX 2: keep original "Rule of 2" behavior to avoid +1 day overshoot
|
||
;; ;; (let* ((end-ts (ts-adjust 'day (- total-days 2) start-ts))
|
||
;; ;; (p2 (save-excursion
|
||
;; ;; (elgantt--goto-date (ts-format "%Y-%m-%d" end-ts))
|
||
;; ;; (point))))
|
||
;; ;; (when (numberp p2)
|
||
;; ;; (elgantt--draw-gradient
|
||
;; ;; (car colors) (cadr colors)
|
||
;; ;; (truncate p1) (1+ (truncate p2)) nil
|
||
;; ;; `(priority ,(setq elgantt-user-set-color-priority-counter
|
||
;; ;; (1- elgantt-user-set-color-priority-counter))
|
||
;; ;; :elgantt-user-overlay ,elgantt-org-id))))))))))
|
||
|
||
;; (elgantt-create-display-rule draw-blocker-lines
|
||
;; :parser ((blocker-raw . ((org-entry-get (point) "BLOCKER"))))
|
||
;; :args (elgantt-org-id elgantt-scheduled)
|
||
;; :body ((when (and elgantt-org-id blocker-raw (not (string-empty-p blocker-raw)))
|
||
;; ;; 1. GET DESTINATION (Start of current task)
|
||
;; ;; We use the built-in elgantt-scheduled arg if available, it's faster and safer.
|
||
;; (let* ((p-dest (save-excursion
|
||
;; (let ((d-start (or (when (stringp elgantt-scheduled) (substring elgantt-scheduled 0 10))
|
||
;; (elgantt-with-point-at-orig-entry nil
|
||
;; (save-excursion
|
||
;; (org-back-to-heading t)
|
||
;; (when (re-search-forward "<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" (line-end-position) t)
|
||
;; (match-string 1)))))))
|
||
;; (when (and d-start (elgantt--goto-date d-start)) (point))))))
|
||
|
||
;; (when (numberp p-dest)
|
||
;; (let ((ids-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw))
|
||
;; (id-list (split-string (if (string-match "ids(\\(.*?\\))" blocker-raw) (match-string 1 blocker-raw) blocker-raw) "[ ,]+" t)))
|
||
;; (dolist (blocker-id id-list)
|
||
;; (save-excursion
|
||
;; (when (elgantt--goto-id blocker-id)
|
||
;; (let ((d-end-str nil)
|
||
;; (row-start (line-beginning-position))
|
||
;; (row-end (line-end-position)))
|
||
;; ;; 2. GET BLOCKER END DATE
|
||
;; (elgantt-with-point-at-orig-entry nil
|
||
;; (save-excursion
|
||
;; (org-back-to-heading t)
|
||
;; (let ((limit (save-excursion (outline-next-heading) (point))))
|
||
;; (if (re-search-forward "<[^>]+>--<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" limit t)
|
||
;; (setq d-end-str (match-string 1))
|
||
;; (let ((s (org-entry-get (point) "SCHEDULED"))
|
||
;; (e (org-entry-get (point) "EFFORT"))
|
||
;; (w (string-to-number (or (org-entry-get (point) "WEEKEND_DAYS") "0"))))
|
||
;; (when (and s e)
|
||
;; (setq d-end-str (ts-format "%Y-%m-%d" (ts-adjust 'day (1- (+ (ceiling (/ (float (org-duration-to-minutes e)) 1440.0)) w)) (ts-parse s))))))))))
|
||
|
||
;; ;; 3. DRAW
|
||
;; (when d-end-str
|
||
;; (save-excursion
|
||
;; (elgantt--goto-date d-end-str)
|
||
;; (let ((p-source (point)))
|
||
;; (if (and (>= p-source row-start) (<= p-source row-end))
|
||
;; (elgantt--draw-line (truncate p-source) (truncate p-dest) "#b8bb26")
|
||
;; ;; Force to row if it jumped
|
||
;; (let ((col-offset (- p-source (save-excursion (goto-char p-source) (line-beginning-position)))))
|
||
;; (goto-char row-start)
|
||
;; (forward-char col-offset)
|
||
;; (elgantt--draw-line (point) (truncate p-dest) "#b8bb26")))))))))))))))
|
||
;; )
|
||
|
||
;; (defun elgantt-open-current-org-file ()
|
||
;; (interactive)
|
||
;; (if-let ((file (buffer-file-name)))
|
||
;; (progn
|
||
;; (setq elgantt-agenda-files (list file))
|
||
;; (elgantt--reset-org-ql-cache)
|
||
;; (elgantt-open))
|
||
;; (message "No file!")))
|
||
|
||
(setq org-roam-directory (file-truename "~/ExoKortex/")
|
||
org-roam-db-location (file-truename "~/ExoKortex/2-Areas/IT/Roam/org-roam.db")
|
||
org-attach-id-dir "assets/"
|
||
org-roam-dailies-directory "~/ExoKortex/2-Areas/Meta-Planning/Org/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")
|
||
)
|
||
|
||
;; (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)
|
||
|
||
(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"))
|
||
|
||
(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)
|
||
|
||
(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))
|
||
|
||
(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-script-path "~/ExoKortex/1-Projects/Exit_strat/robot_program_formater/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 "python " 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 "python " 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"))
|
||
)
|
||
|
||
(diff-hl-mode +1)
|
||
|
||
;; 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
|
||
mistralai/devstral-2512:free
|
||
qwen/qwen3-coder:free))
|
||
(gptel-make-gemini "Gemini"
|
||
:key (auth-source-passage-get 'secret "gemini")
|
||
:stream t
|
||
:models '(gemini-3-pro-preview
|
||
gemini-2.5-pro
|
||
gemini-3-pro
|
||
gemini-2.5-flash))
|
||
(gptel-make-ollama "Ollama"
|
||
:host "localhost:11434"
|
||
:stream t
|
||
:models '(deepseek-r1:1.5b
|
||
gemini-3-pro-preview
|
||
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"))
|
||
(gptel-make-openai "MistralLeChat"
|
||
:host "api.mistral.ai"
|
||
:endpoint "/v1/chat/completions"
|
||
:protocol "https"
|
||
:key (auth-source-passage-get 'secret "mistral")
|
||
:models '("devstral-2512"))
|
||
(setq-default gptel-backend (gptel-get-backend "MistralLeChat")
|
||
gptel-model 'devstral-2512))
|
||
|
||
(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
|
||
))
|
||
|
||
(defun gortium/magit-gptel-generate-commit ()
|
||
"Generate a professional Git commit message from staged changes."
|
||
(interactive)
|
||
|
||
(unless (and buffer-file-name
|
||
(string-match-p "COMMIT_EDITMSG" buffer-file-name))
|
||
(user-error "Not in a commit message buffer"))
|
||
|
||
(let ((diff (magit-git-string "diff" "--cached"))
|
||
(buf (current-buffer)))
|
||
(when (or (null diff) (string-empty-p diff))
|
||
(user-error "No staged changes"))
|
||
|
||
(gptel-request
|
||
(format
|
||
"You are a professional software engineer writing a Git commit message.
|
||
|
||
IMPORTANT RULES:
|
||
- You MUST only use information contained in the diff below.
|
||
- Do NOT guess or invent changes.
|
||
- If the diff is unclear, write a generic message such as:
|
||
'Update files with staged changes' and do NOT mention specific file details.
|
||
- The first line must be imperative and <= 50 characters.
|
||
- Provide a short bullet list in the body (2–4 bullets max).
|
||
|
||
DIFF:
|
||
%s"
|
||
diff)
|
||
:stream nil
|
||
:callback
|
||
(lambda (response _info)
|
||
(with-current-buffer buf
|
||
(erase-buffer)
|
||
(insert (string-trim response) "\n"))))))
|
||
|
||
(map! :after magit
|
||
:map git-commit-mode-map
|
||
:localleader
|
||
:desc "GPT generate commit"
|
||
"g" #'gortium/magit-gptel-generate-commit)
|
||
|
||
;; 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)
|
||
|
||
(after! tramp
|
||
(setq tramp-verbose 10)
|
||
;; Custom TRAMP configuration for Windows SSH ;; (add-to-list 'tramp-connection-properties
|
||
;; (list "ssh"
|
||
;; "Ingenuity-win"
|
||
;; "tramp-shell-prompt-pattern"
|
||
;; "\\$ $"))
|
||
)
|
||
|
||
;; Dirvish config
|
||
(after! dirvish
|
||
;; Display icons, file size, timestamps, etc.
|
||
(setq dirvish-attributes
|
||
'(nerd-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/Org/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")
|
||
)))
|
||
|
||
;; Use `p` to open the yank menu
|
||
(map! :after dirvish-yank
|
||
:map dirvish-mode-map
|
||
:n "p" #'dirvish-yank-menu)
|
||
|
||
;; 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-star 'replace)
|
||
(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 "#fb4934" :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
|
||
(setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter)
|
||
(setq mu4e-contexts
|
||
(list
|
||
(make-mu4e-context
|
||
:name "TDNDE"
|
||
:match-func
|
||
(lambda (msg)
|
||
(when msg
|
||
(string-prefix-p
|
||
"/TDNDE/"
|
||
(mu4e-message-field msg :maildir))))
|
||
:vars
|
||
'((user-mail-address . "tpouplier@tdnde.com")
|
||
(user-full-name . "Thierry Pouplier")
|
||
|
||
;; Folders
|
||
(mu4e-sent-folder . "/TDNDE/Sent")
|
||
(mu4e-drafts-folder . "/TDNDE/Drafts")
|
||
(mu4e-trash-folder . "/TDNDE/Trash")
|
||
(mu4e-refile-folder . "/TDNDE/Archive")
|
||
|
||
;; Shortcuts
|
||
(mu4e-maildir-shortcuts
|
||
.
|
||
(("/TDNDE/Inbox" . ?i)
|
||
("/TDNDE/Sent" . ?s)
|
||
("/TDNDE/Drafts" . ?d)
|
||
("/TDNDE/Trash" . ?t)
|
||
("/TDNDE/Archive" . ?a)))
|
||
|
||
;; Bookmarks
|
||
(mu4e-bookmarks
|
||
.
|
||
((:name "Airbus-6825"
|
||
:query "maildir:/TDNDE/* AND label:proj:6825 AND label:client:airbus"
|
||
:key ?A)
|
||
(:name "Bombardier-3154"
|
||
:query "maildir:/TDNDE/* AND label:proj:3154 AND label:client:bombardier"
|
||
:key ?B)
|
||
(:name "Daher-5304"
|
||
:query "maildir:/TDNDE/* AND label:proj:5304 AND label:client:daher"
|
||
:key ?D)
|
||
(:name "TDNDE"
|
||
:query "maildir:/TDNDE/* AND label:tdnde"
|
||
:key ?T)))
|
||
|
||
;; SMTP
|
||
(smtpmail-smtp-user . "tpouplier@tdnde.com")
|
||
(smtpmail-stream-type . ssl)
|
||
(smtpmail-smtp-server . "smtp.hostinger.com")
|
||
(smtpmail-smtp-service . 465)))
|
||
;; Other context here
|
||
;; (make-mu4e-context ...)
|
||
)
|
||
)
|
||
|
||
(setq mu4e-context-policy 'pick-first
|
||
mu4e-compose-context-policy 'ask)
|
||
|
||
(require 'mu4e-icalendar)
|
||
(mu4e-icalendar-setup)
|
||
|
||
(setq mu4e-icalendar-diary-file nil)
|
||
|
||
(setq gnus-icalendar-org-capture-file +org-capture-journal-file
|
||
gnus-icalendar-org-capture-headline '("Journal"))
|
||
|
||
(gnus-icalendar-org-setup)
|
||
)
|
||
|
||
(after! mu4e
|
||
(setq mu4e-modeline-support nil))
|
||
|
||
(after! mu4e
|
||
;; restore label key
|
||
(evil-define-key 'normal mu4e-headers-mode-map"l" #'mu4e-headers-mark-for-label)
|
||
(evil-define-key 'visual mu4e-headers-mode-map "l" #'mu4e-headers-mark-for-label)
|
||
(evil-define-key 'emacs mu4e-headers-mode-map "l" #'mu4e-headers-mark-for-label)
|
||
|
||
(evil-define-key 'normal mu4e-view-mode-map "l" #'mu4e-view-mark-for-label)
|
||
(evil-define-key 'visual mu4e-view-mode-map "l" #'mu4e-view-mark-for-label)
|
||
(evil-define-key 'emacs mu4e-view-mode-map "l" #'mu4e-view-mark-for-label)
|
||
)
|
||
|
||
(defun gortium-circe-nickserv-password (server)
|
||
(auth-source-passage-get 'secret "irc"))
|
||
|
||
(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"
|
||
:nickserv-password gortium-circe-nickserv-password
|
||
:channels ("#emacs-circe")
|
||
)))
|
||
)
|
||
|
||
;; Enable midnight mode by default for PDF files
|
||
(use-package! pdf-tools
|
||
:magic ("%PDF" . pdf-view-mode)
|
||
:config
|
||
;; Install the server binary if needed (permanent)
|
||
(pdf-tools-install :no-query)
|
||
|
||
;; Hooks
|
||
(add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)
|
||
(add-hook 'pdf-view-mode-hook #'pdf-view-roll-minor-mode)
|
||
)
|
||
|
||
;; 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)
|
||
|
||
;;; ============================================================
|
||
;;; GORTIUM — Org chain scheduler (Safe & Optimized with Debugging)
|
||
;;; ============================================================
|
||
|
||
(require 'org)
|
||
(require 'org-id)
|
||
(require 'cl-lib)
|
||
(require 's)
|
||
|
||
;; ------------------------------------------------------------
|
||
;; Helper: Skip weekends (snap to next weekday at 08:00)
|
||
;; ------------------------------------------------------------
|
||
(defun gortium/org--skip-weekend (time)
|
||
(let* ((decoded (decode-time time))
|
||
(dow (nth 6 decoded)))
|
||
(cond
|
||
((= dow 6) ;; Saturday
|
||
(apply #'encode-time
|
||
(append '(0 0 8)
|
||
(nthcdr 3 (decode-time (time-add time (days-to-time 2)))))))
|
||
((= dow 0) ;; Sunday
|
||
(apply #'encode-time
|
||
(append '(0 0 8)
|
||
(nthcdr 3 (decode-time (time-add time (days-to-time 1)))))))
|
||
(t time))))
|
||
|
||
;; ------------------------------------------------------------
|
||
;; Helper: Snap to working hours (08:00–16:00)
|
||
;; ------------------------------------------------------------
|
||
(defun gortium/internal--snap-to-working-hours (time)
|
||
(let* ((day-start 8)
|
||
(day-end 16)
|
||
(t1 (gortium/org--skip-weekend time))
|
||
(d (decode-time t1))
|
||
(h (nth 2 d)))
|
||
(cond
|
||
((< h day-start)
|
||
(apply #'encode-time (append (list 0 0 day-start) (nthcdr 3 d))))
|
||
((>= h day-end)
|
||
(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)))))))
|
||
(t t1))))
|
||
|
||
(defun gortium/org--get-range-start (pos)
|
||
"Extract the start timestamp from an existing range like <A>--<B>.
|
||
Returns nil if no range found (safe, non-blocking)."
|
||
(save-excursion
|
||
(goto-char pos)
|
||
(org-back-to-heading t)
|
||
(let ((end (save-excursion
|
||
(or (ignore-errors (outline-next-heading))
|
||
(point-max))
|
||
(point)))
|
||
(start-time nil))
|
||
(save-restriction
|
||
(narrow-to-region (point) end)
|
||
(goto-char (point-min))
|
||
;; Look for range in first 50 lines only (safety limit)
|
||
(let ((search-limit (save-excursion
|
||
(forward-line 50)
|
||
(point))))
|
||
(when (re-search-forward "<\\([^>]+\\)>--<[^>]+>" search-limit t)
|
||
(condition-case nil
|
||
(setq start-time (org-time-string-to-time (match-string 1)))
|
||
(error nil)))))
|
||
start-time)))
|
||
|
||
(defun gortium/internal--calculate-task-span (start-time effort-str)
|
||
"Return a list (END-TIME WEEKEND-DAYS) for given START-TIME and EFFORT string."
|
||
(if (or (null effort-str) (string-empty-p effort-str))
|
||
(list start-time 0)
|
||
(let* ((eff-mins (org-duration-to-minutes effort-str))
|
||
(total-work-mins (if (string-match-p "d" effort-str)
|
||
(* (/ eff-mins 1440.0) 480)
|
||
eff-mins))
|
||
(cursor start-time)
|
||
(wknd-count 0)
|
||
(day-start 8)
|
||
(day-end 16)
|
||
(safety-counter 0)
|
||
(max-iterations 1000)) ;; Safety limit
|
||
(while (and (> total-work-mins 0) (< safety-counter max-iterations))
|
||
(setq safety-counter (1+ safety-counter))
|
||
(let* ((decoded (decode-time cursor))
|
||
(h (nth 2 decoded))
|
||
(m (nth 1 decoded))
|
||
(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
|
||
((or (= dow 6) (= dow 0)) ;; weekend
|
||
(setq wknd-count (1+ wknd-count))
|
||
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||
(nthcdr 3 (decode-time
|
||
(time-add cursor (days-to-time 1))))))))
|
||
((<= mins-left-today 0) ;; after hours
|
||
(setq cursor (apply #'encode-time (append (list 0 0 day-start)
|
||
(nthcdr 3 (decode-time
|
||
(time-add cursor (days-to-time 1))))))))
|
||
((<= 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))))))))
|
||
|
||
(when (>= safety-counter max-iterations)
|
||
(message "WARNING: calculate-task-span hit iteration limit for effort %s" effort-str))
|
||
|
||
(list cursor wknd-count))))
|
||
|
||
;; ------------------------------------------------------------
|
||
;; Helper: Find dependency end time
|
||
;; ------------------------------------------------------------
|
||
(defun gortium/internal--get-blocker-end (blocker-str task-end-map)
|
||
"Return latest end time only if blockers are DONE or have been recalculated."
|
||
(let ((clean (s-trim (format "%s" blocker-str)))
|
||
(latest-time nil)
|
||
(all-resolved t))
|
||
(when (and (string-match "ids(\\(.*?\\))" clean)
|
||
(not (string-empty-p (s-trim (match-string 1 clean)))))
|
||
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
|
||
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid))
|
||
(pos (org-id-find clean-id t))
|
||
(computed-end (gethash clean-id task-end-map))
|
||
(blocker-end
|
||
(cond
|
||
;; 1) Use the new time we just calculated in this run (Priority!)
|
||
(computed-end computed-end)
|
||
|
||
;; 2) If it's DONE, use the CLOSED timestamp
|
||
((and pos (org-entry-get pos "CLOSED"))
|
||
(org-time-string-to-time (org-entry-get pos "CLOSED")))
|
||
|
||
;; 3) If it's FIXED, use the existing range/scheduled time
|
||
((and pos (string-equal "t" (org-entry-get pos "FIXED")))
|
||
(or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback
|
||
(org-get-scheduled-time pos)))
|
||
|
||
;; Otherwise: We MUST wait for this blocker to be recalculated
|
||
(t nil))))
|
||
|
||
(if blocker-end
|
||
(setq latest-time (if (or (null latest-time) (time-less-p latest-time blocker-end))
|
||
blocker-end latest-time))
|
||
(setq all-resolved nil)))))
|
||
(when all-resolved latest-time)))
|
||
|
||
(defun gortium/internal--update-properties (pos start wknd id end task-end-map)
|
||
"Heals the property drawer, updates values, and fixes vertical spacing without leaking newlines."
|
||
(message "[DEBUG] enter update-properties for %s" id)
|
||
(save-excursion
|
||
(goto-char pos)
|
||
(org-back-to-heading t)
|
||
(let* ((subtree-start (point))
|
||
(subtree-end (save-excursion (org-end-of-subtree t) (point))))
|
||
|
||
(save-restriction
|
||
(narrow-to-region subtree-start subtree-end)
|
||
|
||
;; --- STEP 1: HEAL THE DRAWER ---
|
||
(goto-char (point-min))
|
||
(forward-line 1)
|
||
(while (looking-at "^[ \t]*\\(CLOSED:\\|SCHEDULED:\\|DEADLINE:\\)")
|
||
(forward-line 1))
|
||
|
||
(when (looking-at "^[ \t]*:PROPERTIES:[ \t]*$")
|
||
(let ((drawer-start (point)))
|
||
(when (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)
|
||
(let ((drawer-end (match-end 0)))
|
||
(save-restriction
|
||
(narrow-to-region drawer-start drawer-end)
|
||
(goto-char (point-min))
|
||
(forward-line 1)
|
||
(while (re-search-forward "^[ \t]*$" nil t)
|
||
(delete-region (line-beginning-position)
|
||
(min (1+ (line-end-position)) (point-max)))))))))
|
||
|
||
;; --- STEP 2: UPDATE PROPERTY ---
|
||
(org-entry-put nil "WEEKEND_DAYS" (number-to-string (or wknd 0)))
|
||
|
||
;; --- STEP 3: REMOVE OLD RANGE ---
|
||
(goto-char (point-min))
|
||
(while (re-search-forward "^[ \t]*<.+>--<.+>[ \t]*\n?" nil t)
|
||
(replace-match ""))
|
||
|
||
;; --- STEP 4: FIND INSERTION POINT ---
|
||
(goto-char (point-min))
|
||
(forward-line 1)
|
||
(while (looking-at "^[ \t]*\\(CLOSED:\\|SCHEDULED:\\|DEADLINE:\\)")
|
||
(forward-line 1))
|
||
(while (looking-at "^[ \t]*:\\([A-Z_]+\\):[ \t]*$")
|
||
(when (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)
|
||
(forward-line 1)))
|
||
|
||
;; Delete any existing blank lines at the insertion point
|
||
(while (and (looking-at "^[ \t]*$") (not (eobp)))
|
||
(delete-region (line-beginning-position) (line-beginning-position 2)))
|
||
|
||
;; --- STEP 5: INSERT RANGE ---
|
||
;; Ensure range starts on a new line and ends with exactly one newline
|
||
(unless (bolp) (insert "\n"))
|
||
(insert (format "<%s>--<%s>\n"
|
||
(format-time-string "%Y-%m-%d %a %H:%M" start)
|
||
(format-time-string "%Y-%m-%d %a %H:%M" end)))
|
||
|
||
;; --- STEP 6: CLEAN UP REMAINING WHITESPACE WITHIN TASK ---
|
||
(while (and (looking-at "^[ \t]*$") (not (eobp)))
|
||
(delete-region (line-beginning-position) (line-beginning-position 2))))
|
||
|
||
;; --- STEP 7: NORMALIZE SPACING BETWEEN TASKS ---
|
||
;; Instead of inserting a newline blindly, we ensure exactly one blank line
|
||
;; exists between this subtree's end and the next heading.
|
||
(goto-char (org-end-of-subtree t))
|
||
(let ((post-subtree (point)))
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char post-subtree)
|
||
(delete-blank-lines)
|
||
;; Only insert a blank line if we aren't at the end of the buffer
|
||
(unless (eobp)
|
||
(insert "\n")))))
|
||
|
||
(message "[DEBUG] exit update-properties")
|
||
(puthash id end task-end-map)))
|
||
|
||
;; ------------------------------------------------------------
|
||
;; Helper: Detect circular dependencies
|
||
;; ------------------------------------------------------------
|
||
(defun gortium/internal--detect-circular-deps (tasks)
|
||
"Check for circular dependencies in TASKS.
|
||
Returns list of task IDs involved in cycles, or nil if no cycles found."
|
||
(let ((graph (make-hash-table :test 'equal))
|
||
(visiting (make-hash-table :test 'equal))
|
||
(visited (make-hash-table :test 'equal))
|
||
(cycles nil))
|
||
|
||
;; Build dependency graph
|
||
(dolist (task tasks)
|
||
(pcase-let ((`(,_pos ,id ,_effort ,blocker ,_fixed ,_sched ,_rng-start ,_offset ,_state) task))
|
||
(when (and blocker (not (string-empty-p (s-trim blocker))))
|
||
(when (string-match "ids(\\(.*?\\))" blocker)
|
||
(let ((deps (split-string (match-string 1 blocker) "[ ,]+" t)))
|
||
(puthash id (mapcar (lambda (tid)
|
||
(replace-regexp-in-string "[\"']\\|id:" "" tid))
|
||
deps)
|
||
graph))))))
|
||
|
||
;; DFS to detect cycles
|
||
(cl-labels ((dfs (node path)
|
||
(cond
|
||
((gethash node visiting)
|
||
;; Found a cycle
|
||
(push node cycles)
|
||
t)
|
||
((gethash node visited)
|
||
nil)
|
||
(t
|
||
(puthash node t visiting)
|
||
(dolist (dep (gethash node graph))
|
||
(when (dfs dep (cons node path))
|
||
(push node cycles)))
|
||
(remhash node visiting)
|
||
(puthash node t visited)
|
||
nil))))
|
||
|
||
(maphash (lambda (node _deps)
|
||
(unless (gethash node visited)
|
||
(dfs node nil)))
|
||
graph))
|
||
|
||
(delete-dups cycles)))
|
||
|
||
(advice-add 'org-roam-db-sync :before
|
||
(lambda (&rest _)
|
||
(message "[DEBUG] org-roam-db-sync invoked")))
|
||
|
||
;; --- MAIN SCHEDULER ---
|
||
(defun gortium/org-schedule-subtree-chains ()
|
||
"Optimized scheduler: Ignores stale buffer ranges to ensure correct dependency flow."
|
||
(interactive)
|
||
(message "=== Starting Gortium Scheduler ===")
|
||
|
||
;; 1. Global deactivations to prevent the "Parser Error"
|
||
(let ((org-element-use-cache nil)
|
||
(all-tasks '())
|
||
(task-end-times (make-hash-table :test 'equal))
|
||
(start-time (current-time)))
|
||
|
||
;; 2. COLLECTION
|
||
(org-map-entries
|
||
(lambda ()
|
||
(when (org-get-todo-state)
|
||
(let* ((pos (point-marker))
|
||
(id (or (org-id-get) (org-id-get-create))))
|
||
(push (list (current-buffer) pos id
|
||
(org-entry-get pos "EFFORT")
|
||
(org-entry-get pos "BLOCKER")
|
||
(org-entry-get pos "FIXED")
|
||
(org-get-scheduled-time pos)
|
||
(org-entry-get pos "OFFSET_DAYS"))
|
||
all-tasks))))
|
||
nil nil)
|
||
|
||
(setq all-tasks (nreverse all-tasks))
|
||
|
||
;; 3. THE LOOP
|
||
(let* ((remaining all-tasks)
|
||
(limit (* 20 (length remaining)))
|
||
(iter 0))
|
||
|
||
(while (and remaining (< iter limit))
|
||
(setq iter (1+ iter))
|
||
(let ((done-this-loop '()))
|
||
(dolist (task remaining)
|
||
(pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task))
|
||
(let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times))
|
||
(has-blocker (and blocker (not (string-empty-p (s-trim blocker)))))
|
||
;; A task is ready if it's FIXED or all blockers are in the task-end-times map
|
||
(is-fixed (string-equal fixed "t"))
|
||
(ready (or is-fixed (not has-blocker) blocker-end)))
|
||
|
||
(when ready
|
||
(with-current-buffer buf
|
||
(org-element-with-disabled-cache
|
||
(let* ((off-days (if (stringp offset) (string-to-number offset) 0))
|
||
(base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time)))
|
||
(t (or blocker-end sched (current-time)))))
|
||
(final-start (if is-fixed base-start
|
||
(gortium/internal--snap-to-working-hours (time-add base-start (days-to-time off-days)))))
|
||
(span (gortium/internal--calculate-task-span final-start effort))
|
||
(final-end (car span))
|
||
(wknd (cadr span)))
|
||
|
||
(gortium/internal--update-properties pos final-start wknd id final-end task-end-times)
|
||
(push task done-this-loop))))))))
|
||
(setq remaining (cl-set-difference remaining done-this-loop))))
|
||
|
||
(org-element-cache-reset 'all)
|
||
(message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter))))
|
||
|
||
|
||
;; ---------------------------------------------
|
||
(defun gortium/org-ensure-task-properties ()
|
||
"Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)
|
||
and ensure the standard property drawer exists without overwriting existing data."
|
||
(interactive)
|
||
(save-excursion
|
||
(message "--- Initializing Task Properties for all states ---")
|
||
(let ((count 0)
|
||
;; List of properties to ensure exist
|
||
(props '("EFFORT" "BLOCKER" "FIXED" "WEEKEND_DAYS"
|
||
"ASSIGNEE" "RESOURCES" "CATEGORY"
|
||
"DIMENTIONS" "WEIGHT" "OFFSET_DAYS")))
|
||
(org-map-entries
|
||
(lambda ()
|
||
;; This check returns true if the heading has ANY todo keyword
|
||
(let ((todo-state (org-get-todo-state)))
|
||
(when todo-state
|
||
(cl-incf count)
|
||
;; 1. Handle ID (builtin handles 'do not replace' logic)
|
||
(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 ()
|
||
"Add IDs to all headings in current subtree."
|
||
(interactive)
|
||
(save-excursion
|
||
(org-back-to-heading t)
|
||
(org-map-entries
|
||
(lambda () (org-id-get-create))
|
||
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 gortium/main-frame nil
|
||
"The main Emacs frame where buffers should be toggled to/from.")
|
||
|
||
(add-hook 'emacs-startup-hook
|
||
(lambda () (setq gortium/main-frame (selected-frame))))
|
||
|
||
(defun gortium--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 gortium--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 gortium/toggle-buffer-to-frame ()
|
||
"Toggle the current buffer between `gortium/main-frame' and a new frame."
|
||
(interactive)
|
||
(unless (frame-live-p gortium/main-frame)
|
||
(setq gortium/main-frame (selected-frame)))
|
||
|
||
(let* ((buf (current-buffer))
|
||
(old-frame (selected-frame))
|
||
(old-win (selected-window)))
|
||
(if (eq old-frame gortium/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 (> (gortium--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 gortium/main-frame
|
||
(let ((target (gortium--rightmost-non-minibuffer-window gortium/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 (= (gortium--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-projects "~/ExoKortex/2-Areas/Meta-Planning/Org/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-projects` 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-projects))
|
||
(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-projoects`."
|
||
(interactive "sOrg file name (without .org): ")
|
||
(let* ((org-file (concat file-name ".org"))
|
||
(org-real (expand-file-name org-file gortium/org-repo-projects))
|
||
(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-projects`."
|
||
(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-projects)))
|
||
(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))))))
|
||
|
||
(defun gortium/refile-to-today-daily ()
|
||
"Refile the current subtree under the 'Journal' headline in today's daily note.
|
||
This version uses `org-roam-dailies-capture-today` to ensure the daily note
|
||
is created with Org-roam’s templates (including :ID:)."
|
||
(interactive)
|
||
(require 'org-roam-dailies)
|
||
|
||
(let ((headline "Journal"))
|
||
|
||
;; Step 1: Ensure today’s daily exists via org-roam (guarantees :ID:)
|
||
(let ((daily-file (org-roam-dailies-capture-today nil)))
|
||
;; Step 2: Cut the subtree (use org-copy-subtree if you prefer duplication)
|
||
(org-cut-subtree)
|
||
|
||
;; Step 3: Paste under the Journal headline
|
||
(with-current-buffer (find-file-noselect daily-file)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (concat "^\\*+ " (regexp-quote headline) "\\b") nil t)
|
||
(let ((parent-level (org-current-level)))
|
||
(end-of-line)
|
||
(insert "\n")
|
||
(org-paste-subtree (+ 1 parent-level))))))
|
||
(message "Task refiled under '%s' in today’s daily note." headline))))
|
||
|
||
(defun gortium/refile-to-daily-of-close ()
|
||
"Refile the current subtree under the 'Journal' headline in the daily note
|
||
corresponding to the task's CLOSED date. If no CLOSED property is found,
|
||
fallback to today's daily note. Ensures the daily has an Org-roam ID."
|
||
(interactive)
|
||
(require 'org-roam-dailies)
|
||
|
||
;; Step 1: Get CLOSED property timestamp
|
||
(let* ((closed-prop (org-entry-get (point) "CLOSED"))
|
||
(time (when closed-prop
|
||
(ignore-errors (org-time-string-to-time closed-prop))))
|
||
(date (or time (current-time)))
|
||
(headline "Journal"))
|
||
|
||
;; Step 2: Ensure daily file exists using org-roam (this guarantees :ID: etc.)
|
||
(let ((daily-file (org-roam-dailies-capture-date date nil)))
|
||
;; Step 3: Cut the subtree.
|
||
(org-cut-subtree)
|
||
|
||
;; Step 4: Paste into the proper daily file
|
||
(with-current-buffer (find-file-noselect daily-file)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
;; Find the 'Journal' headline
|
||
(when (re-search-forward (concat "^\\*+ " (regexp-quote headline) "\\b") nil t)
|
||
(let ((parent-level (org-current-level)))
|
||
(end-of-line)
|
||
(insert "\n")
|
||
(org-paste-subtree (+ 1 parent-level))))))
|
||
|
||
(message "Task refiled under '%s' in %s"
|
||
headline
|
||
(file-name-nondirectory daily-file)))))
|
||
|
||
;; Org-edna for advanced task dependencies
|
||
(use-package! org-edna
|
||
:after org
|
||
:config
|
||
(setq org-edna-use-inheritance t)
|
||
(org-edna-mode 1)
|
||
(map! :map org-mode-map
|
||
:localleader
|
||
:desc "Edit Edna rules" "E" #'org-edna-edit))
|