123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- ;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
- (setq user-full-name "Colin Powell"
- user-mail-address "colin@unbl.ink")
- (nyan-mode)
- (setq doom-theme 'doom-one
- doom-font (font-spec :family "Iosevka" :size 14 :weight 'regular)
- doom-big-font (font-spec :family "Iosevka" :size 18 :weight 'regular)
- doom-variable-pitch-font (font-spec :family "Overpass" :size 12))
- (setq display-line-numbers-type t)
- ;; change `org-directory'. It must be set before org loads!
- (setq org-directory "~/var/org/")
- (load! "+agenda-fix")
- (defun vulpea-agenda-files-update (&rest _)
- (setq org-agenda-files vulpea-project-files))
- (setq org-roam-directory "~/var/org/"
- org-roam-dailies-directory "dailies")
- (advice-add 'org-agenda :before #'vulpea-agenda-files-update)
- (advice-add 'org-todo-list :before #'vulpea-agenda-files-update)
- (map! ;; Easier window movement
- :n "C-h" 'evil-window-left
- :n "C-j" 'evil-window-down
- :n "C-k" 'evil-window-up
- :n "C-l" 'evil-window-right
- (:map evil-treemacs-state-map
- "C-h" 'evil-window-left
- "C-l" 'evil-window-right)
- :leader
- (:prefix "f"
- :desc "Find file in dotfiles" "t" #'+hlissner/find-in-dotfiles
- :desc "Browse dotfiles" "T" #'+hlissner/browse-dotfiles)
- (:prefix "b"
- :desc "Black format buffer" "f" #'blacken-buffer
- :desc "isort buffer" "I" #'py-isort-buffer
- :desc "Links in buffer" "l" #'ace-link-org))
- (defun unfill-paragraph ()
- "Takes a multi-line paragraph and makes it into a single line of text."
- (interactive)
- (let ((fill-column (point-max)))
- (fill-paragraph nil)))
- (define-key global-map "\M-z" 'unfill-paragraph)
- (defun file-notify-rm-all-watches ()
- "Remove all existing file notification watches from Emacs."
- (interactive)
- (maphash
- (lambda (key _value)
- (file-notify-rm-watch key))
- file-notify-descriptors))
- (setq frame-title-format
- '(""
- (:eval
- (if (s-contains-p org-roam-directory (or buffer-file-name ""))
- (replace-regexp-in-string
- ".*/[0-9]*-?" "☰ "
- (subst-char-in-string ?_ ? buffer-file-name))
- "%b"))
- (:eval
- (let ((project-name (projectile-project-name)))
- (unless (string= "-" project-name)
- (format (if (buffer-modified-p) " ◉ %s" " ● %s") project-name))))))
- (setq mm-text-html-renderer 'w3m)
- (setq w3m-fill-column 88)
- (setq lsp-lens-enable 1
- lsp-ui-sideline-enable 1
- lsp-enable-links 1
- lsp-headerline-breadcrumb-enable 1
- lsp-modeline-code-actions-enable 1
- lsp-modeline-diagnostics-enable 1
- lsp-completion-show-detail 1
- lsp-file-watch-threshold nil)
- ;; check for hosts folder and find any init-HOSTNAME.el files in there and load them
- (defvar host (substring (shell-command-to-string "hostname") 0 -1))
- (defvar host-dir "~/.config/doom/hosts/")
- (add-load-path! host-dir)
- ;; Setup nov.el mode for epubs and change font
- (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))
- (defun my-nov-font-setup ()
- (face-remap-add-relative 'variable-pitch :family "Overpass"
- :height 1.0))
- (add-hook 'nov-mode-hook 'my-nov-font-setup)
- ;;(let ((init-host-feature (intern (concat "init-" host ".el"))))
- ;; (load-file init-host-feature))
- (defvar host-init (concat "~/.config/doom/hosts/init-" host ".el"))
- (if (file-exists-p host-init)
- (load-file host-init))
- (load-file "~/.config/doom/+agenda-fix.el")
- ;; Enable org-modern mode per buffer
- ;(add-hook 'org-mode-hook #'org-modern-mode)
- ;(add-hook 'org-agenda-finalize-hook #'org-modern-agenda)
- ;; Or globally
- (with-eval-after-load 'org (global-org-modern-mode))
- (require 'cl-lib)
- (defun eshell-load-bash-aliases ()
- "Read Bash aliases and add them to the list of eshell aliases."
- ;; Bash needs to be run - temporarily - interactively
- ;; in order to get the list of aliases.
- (with-temp-buffer
- (call-process "bash" nil '(t nil) nil "-ci" "alias")
- (goto-char (point-min))
- (cl-letf (((symbol-function 'eshell-write-aliases-list) #'ignore))
- (while (re-search-forward "alias \\(.+\\)='\\(.+\\)'$" nil t)
- (eshell/alias (match-string 1) (match-string 2))))
- (eshell-write-aliases-list)))
- ;; We only want Bash aliases to be loaded when Eshell loads its own aliases,
- ;; rather than every time `eshell-mode' is enabled.
- (add-hook 'eshell-alias-load-hook 'eshell-load-bash-aliases)
- (defun eshell-run-direnv-allow()
- (direnv-allow))
- (add-hook 'eshell-directory-change-hook 'eshell-run-direnv-allow)
- (defun org-raw-timestamp-to-iso (raw-ts)
- "Convert Org RAW-TS like `<2025-06-12 Thu 14:00>` to `YYYY-MM-DDThh:mm:ss`."
- (when raw-ts
- (let* ((ts (org-parse-time-string raw-ts))
- (year (nth 5 ts)) (mon (nth 4 ts)) (day (nth 3 ts))
- (hour (nth 2 ts) 0) (min (nth 1 ts) 0))
- (format "%04d-%02d-%02dT%02d:%02d:00" year mon day hour min))))
- (defun org-extract-labeled-timestamps ()
- "Return an alist of labeled ISO-formatted timestamps in the current Org subtree."
- (save-restriction
- (org-narrow-to-subtree)
- (let ((parsed (org-element-parse-buffer))
- (labeled-ts '()))
- (org-element-map parsed '(timestamp)
- (lambda (ts)
- (let* ((type (org-element-property :type ts))
- (raw (org-element-property :raw-value ts))
- (time (org-parse-time-string raw t))
- (date (format "%04d-%02d-%02d"
- (nth 5 time) (nth 4 time) (nth 3 time)))
- (hour (nth 2 time))
- (min (nth 1 time))
- (with-time (and hour min (format "%sT%02d:%02d" date hour min)))
- (label (cond
- ((eq type 'active) "timestamp")
- ((eq type 'inactive) "inactive-timestamp")
- (t "timestamp"))))
- (push (cons label (or with-time date)) labeled-ts))))
- ;; Add planning info from heading (DEADLINE, SCHEDULED, CLOSED)
- (dolist (key '("DEADLINE" "SCHEDULED" "CLOSED"))
- (let ((raw (org-entry-get nil key t)))
- (when raw
- (let* ((ts (org-parse-time-string raw t))
- (date (format "%04d-%02d-%02d" (nth 5 ts) (nth 4 ts) (nth 3 ts)))
- (hour (nth 2 ts))
- (min (nth 1 ts))
- (with-time (and hour min (format "%sT%02d:%02d" date hour min))))
- (push (cons (downcase key) (or with-time date)) labeled-ts)))))
- (delete-dups labeled-ts))))
- (defun org-get-body ()
- "Return the body text under the current Org heading as a string."
- (save-excursion
- (org-back-to-heading t)
- (let ((start (progn (forward-line) (point)))
- (end (progn (org-end-of-subtree t t) (point))))
- (buffer-substring-no-properties start end))))
- (defun org-strip-timestamps-from-text (text)
- "Remove Org timestamps and planning lines from TEXT."
- (let* ((timestamp-re (rx (or (seq "<" (+ (not (any ">"))) ">")
- (seq "[" (+ (not (any "]"))) "]"))))
- (planning-line-re (rx line-start (zero-or-more space)
- (or "DEADLINE:" "SCHEDULED:" "CLOSED:") " "))
- ;; Step 1: remove full planning lines
- (without-planning-lines
- (replace-regexp-in-string
- (concat planning-line-re ".*\n?") "" text))
- ;; Step 2: remove inline timestamps
- (without-inline
- (replace-regexp-in-string timestamp-re "" without-planning-lines)))
- (string-trim without-inline)))
- (defun org-extract-drawers ()
- "Extract all drawers (like LOGBOOK, PROPERTIES, etc.) from current org entry.
- Returns an alist of (DRAWER-NAME . CONTENT) pairs.
- - PROPERTIES content is parsed into (KEY . VALUE)
- - Other drawers are returned as lists of lines (strings)"
- (save-excursion
- (org-back-to-heading t)
- (let ((end (save-excursion (org-end-of-subtree t t)))
- (drawers '()))
- (while (re-search-forward "^\\s-*:\\([A-Z]+\\):\\s-*$" end t)
- (let* ((name (match-string 1))
- (start (match-end 0))
- (drawer-end (when (re-search-forward "^\\s-*:END:\\s-*$" end t)
- (match-beginning 0))))
- (when drawer-end
- (let ((content (buffer-substring-no-properties start drawer-end)))
- (setq drawers
- (cons
- (cons name
- (if (string= name "PROPERTIES")
- ;; parse :KEY: VALUE
- (org-parse-properties content)
- ;; just return line list
- (split-string content "\n" t "[ \t]+")))
- drawers))))))
- (reverse drawers))))
- (defun org-parse-properties (content)
- "Parse PROPERTIES drawer content into an alist."
- (let ((lines (split-string content "\n" t))
- (props '()))
- (dolist (line lines)
- (when (string-match "^\\s-*:\\([^:]+\\):\\s-*\\(.*\\)$" line)
- (push (cons (match-string 1 line) (match-string 2 line)) props)))
- (reverse props)))
- (defun org-clean-body-text (text)
- "Remove planning lines, timestamps, and drawers from TEXT."
- (let* ((timestamp-re
- (rx (or (seq "<" (+ (not (any ">"))) ">")
- (seq "[" (+ (not (any "]"))) "]"))))
- (planning-re
- (rx line-start (zero-or-more space)
- (or "DEADLINE:" "SCHEDULED:" "CLOSED:") " " (* nonl) "\n"))
- (text (replace-regexp-in-string planning-re "" text))
- (text (replace-regexp-in-string timestamp-re "" text))
- (text (org-strip-all-drawers text)))
- (string-trim text)))
- (defun org-strip-timestamps-drawers-notes-from-text (text)
- "Strip timestamps, planning lines, drawers, and note blocks from Org TEXT."
- (let* ((timestamp-re
- (rx (or (seq "<" (+ (not (any ">"))) ">")
- (seq "[" (+ (not (any "]"))) "]"))))
- (planning-re
- (rx line-start (zero-or-more space)
- (or "DEADLINE:" "SCHEDULED:" "CLOSED:") " " (* nonl) "\n"))
- (drawer-re
- "^\\s-*:[A-Z]+:\\(?:.\\|\n\\)*?:END:\n?")
- (note-block-re
- (rx-to-string
- `(and bol (* space) "- Note taken on "
- (or "[" "<") (+ (not (any "]>"))) (or "]" ">")
- (*? anything)
- (or "\n\n" eos))
- t)))
- ;; Strip drawers first
- (setq text (replace-regexp-in-string drawer-re "" text))
- ;; Strip entire note blocks (greedy match up to next blank line or end)
- (setq text (replace-regexp-in-string note-block-re "" text))
- ;; Strip planning lines and timestamps
- (setq text (replace-regexp-in-string planning-re "" text))
- (setq text (replace-regexp-in-string timestamp-re "" text))
- ;; Trim and return
- (string-trim text)))
- (defun org-get-body-stripped ()
- "Get cleaned Org entry body without timestamps, planning lines, drawers, or notes."
- (org-strip-timestamps-drawers-notes-from-text (org-get-body)))
- (defun org-extract-notes ()
- "Extract notes from Org entry, each as an alist with `timestamp` and `content`."
- (save-excursion
- (org-back-to-heading t)
- (let ((start (progn (forward-line) (point)))
- (end (progn (org-end-of-subtree t t) (point)))
- result) ;; ✅ initialize result list
- (save-restriction
- (narrow-to-region start end)
- (goto-char (point-min))
- (while (re-search-forward "^\\s-*[-+] Note taken on \\[\\([^]]+\\)\\]\\s-*\\(?:\\\\\\\\\\)?\\s-*$" nil t)
- (let* ((raw-ts (match-string 1))
- (timestamp (let* ((ts (org-parse-time-string raw-ts t)))
- (format "%04d-%02d-%02dT%02d:%02d"
- (nth 5 ts) (nth 4 ts) (nth 3 ts)
- (nth 2 ts) (nth 1 ts))))
- (note-start (progn
- (forward-line)
- ;; allow one optional blank line
- (when (looking-at-p "^\\s-*$") (forward-line))
- (point)))
- (note-end (or (save-excursion
- (re-search-forward "^\\s-*[-+] Note taken on \\[" nil t))
- (point-max)))
- (content (string-trim
- (buffer-substring-no-properties note-start (1- note-end)))))
- (push `(("timestamp" . ,timestamp)
- ("content" . ,content))
- result))))
- (nreverse result))))
- (require 'subr-x) ;; for string-trim and string functions, usually loaded by default
- (defun my-org-generate-uuid ()
- "Generate a random UUID string."
- (let ((uuid (md5 (format "%s%s%s%s%s"
- (user-uid)
- (emacs-pid)
- (float-time)
- (random)
- (emacs-pid)))))
- (concat (substring uuid 0 8) "-"
- (substring uuid 8 12) "-"
- (substring uuid 12 16) "-"
- (substring uuid 16 20) "-"
- (substring uuid 20 32))))
- (defun my-org-get-or-create-id ()
- "Get the ID property of the current Org heading, or create and set one if missing.
- Returns the ID string."
- (let ((id (org-entry-get nil "ID")))
- (unless id
- (setq id (my-org-generate-uuid))
- (org-entry-put nil "ID" id)
- (save-buffer)) ;; optional: save file after inserting ID
- id))
- (defun org-clock-on-state-change ()
- "Clock in when state is STRT, clock out otherwise."
- (when (and (derived-mode-p 'org-mode)
- (not (org-before-first-heading-p)))
- (pcase org-state
- ("STRT"
- (unless org-clock-marker
- (org-clock-in)))
- ((or "DONE" "CANC" "WAIT" "HOLD" "TODO")
- (when org-clock-marker
- (org-clock-out))))))
- (defun send-org-todo-to-endpoint-on-state-change ()
- "Send the current Org-mode TODO item to an HTTP endpoint."
- (interactive)
- (when (org-at-heading-p)
- (let ((state (org-get-todo-state)))
- (when (member state '("STRT" "DONE"))
- (let* ((heading (org-get-heading t t t t))
- (current-time (format-time-string "%Y-%m-%dT%H:%M:%SZ" (current-time) t)) ;; UTC ISO8601
- (tags (org-get-tags))
- (timestamps (org-extract-labeled-timestamps))
- (drawers (org-extract-drawers))
- (properties (cdr (assoc "PROPERTIES" drawers)))
- (todo-id (my-org-get-or-create-id))
- (body (org-get-body-stripped))
- (notes (org-extract-notes))
- (properties (org-entry-properties))
- (endpoint "https://life.lab.unbl.ink/webhook/emacs/")
- (data `(("description" . ,heading)
- ("labels" . ,tags)
- ("state" . ,state)
- ("timestamps" . ,timestamps)
- ("notes" . ,notes)
- ("drawers" . ,drawers)
- ("emacs_id" . ,todo-id)
- ("updated_at" . ,current-time)
- ("source" . "orgmode")
- ("properties" . ,properties)
- ("body" . ,body))))
- (request
- endpoint
- :type "POST"
- :headers '(("Content-Type" . "application/json"))
- :data (json-encode data)
- :headers '(("Authorization" . "Token 27a4bde480a982e4e0bc74e9d74d052f071b1737")
- ("Content-Type" . "application/json"))
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (message "Sent TODO: %s" data)))
- :error (cl-function
- (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Error sending TODO: %S" error-thrown))))))
- (org-clock-on-state-change)
- )))
- (add-hook 'org-after-todo-state-change-hook #'send-org-todo-to-endpoint-on-state-change)
|