#+TITLE: Custom Elisp Functions
* Org-Roam
** Refresh Org Roam Agenda

Creates a function to refresh the ~org-agenda-files~ variable to be set to include all org roam notes files.

#+begin_src emacs-lisp
(defun jm/org-roam-refresh-agenda-list ()
  (interactive)
  (org-roam-db-sync)
  (let ((directory (expand-file-name org-roam-dailies-directory org-roam-directory)))
    (setq org-agenda-files
          (seq-filter
           (lambda (file-path) (not (s-starts-with-p directory file-path)))
           (org-roam-list-files)))))
#+end_src

** Custom Org-Roam Indexing Functions

Before getting into the main config for Org-roam, I've created a few functions for better indexing nodes stored in the org-roam database. Specifically, these functions separate the org roam dailies nodes from other nodes.

#+begin_src emacs-lisp
(defun jm/org-roam-find-filter (node)
  (let ((directory (expand-file-name org-roam-dailies-directory org-roam-directory)))
    (string= (file-name-directory (org-roam-node-file node))
             directory)))

(defun jm/org-roam-dailies-find ()
  (interactive)
  (org-roam-node-find nil nil #'jm/org-roam-find-filter))

(defun jm/org-roam-find ()
  (interactive)
  (org-roam-node-find
   nil nil
   (lambda (node) (not (jm/org-roam-find-filter node)))))
#+end_src

** Org Roam Dailies Shortcuts

Shortcut to goto todays org-roam dailies document.

#+begin_src emacs-lisp
(defun jm/dailies-file-p ()
  (when-let ((file (buffer-file-name))
             (file-base (file-name-base (buffer-file-name))))
    (s-matches-p "^[0-9]+-[0-9]+-[0-9]+$" file-base)))

(defun jm/org-roam-capture-today ()
  (interactive)
  (jm/org-roam-goto-day 0 t "t")
  (delete-other-windows))

(defun jm/org-roam-goto-day (days &optional force-capture keys)
  (let* ((base-time
          (if (and (jm/dailies-file-p) (not (eq days 0)))
              (date-to-time (file-name-base (buffer-file-name)))
            (current-time)))
         (rel-time (time-add base-time (days-to-time days)))
         (path (format-time-string "%Y-%m-%d.org" rel-time))
         (full-path (file-name-concat org-roam-directory "daily" path)))
    (jm/org-roam-refresh-agenda-list)
    (if (and (file-exists-p full-path) (not force-capture))
        (find-file full-path)
      (org-roam-dailies--capture rel-time nil keys))))

(jm/leader-keys
  "oy" '((lambda () (interactive) (jm/org-roam-goto-day -1)) :which-key "Open/create yesterday's daily notes file")
  "ot" '((lambda () (interactive) (jm/org-roam-goto-day 0)) :which-key "Open/create today's daily notes file")
  "ok" '((lambda () (interactive) (jm/org-roam-goto-day 1)) :which-key "Open/create tomorrow's daily notes file"))
#+end_src

* Capture Template Functions

These functions are for my org roam daily capture template.

** Helper Functions

#+begin_src emacs-lisp
(defun jm/dt-filter-tasks (helper query)
  (let ((entries (org-map-entries helper query 'agenda)))
    (string-join (delq nil entries) "\n")))

(defun jm/dt-format-link (prefix)
  (let ((item-name (org-entry-get nil "ITEM"))
        (item-id (org-id-get-create))
        (doc-title (org-get-title)))
    (if doc-title
        (format "%s [[id:%s][%s - %s]]" prefix item-id doc-title item-name)
    (format "%s [[id:%s][%s]]" prefix item-id item-name))))

(defun jm/dt-concat-todos (todo-lists)
  (let ((todos nil))
    (dolist (todo todo-lists todos)
      (setq todos (append todos (string-lines todo))))
    (string-join (delete "" (delete-dups todos)) "\n")))
#+end_src

** Queries

#+begin_src emacs-lisp
(defun jm/dt-get-priority (priority &optional prompt)
  (jm/dt-filter-tasks
   (lambda () (when (equal priority (org-entry-get nil "PRIORITY"))
                (jm/dt-format-link (or prompt "+"))))
   "TODO=\"TODO\"|TODO=\"IN PROGRESS\""))

(defun jm/dt-get-status (status &optional prompt)
  (jm/dt-filter-tasks (lambda () (jm/dt-format-link (or prompt "+ [ ]")))
                      (concat "TODO=\"" status "\"")))


(defconst jm/dt-deadline "DEADLINE")
(defconst jm/dt-scheduled "SCHEDULED")

(defun jm/dt-get-within (type days &optional prompt)
  (unless (or (eq type jm/dt-deadline) (eq type jm/dt-scheduled))
    (error "Invalid type for jm/dt-get-within."))
  (let* ((time (or (org-capture-get :default-time) (current-time)))
         (date (time-add time (days-to-time days))))
    (jm/dt-filter-tasks
     (lambda () (when (member (org-get-todo-state) '("TODO" "WAITING" "IN PROGRESS"))
                  (jm/dt-format-link (or prompt "+"))))
     (concat type (format-time-string "<=\"<%Y-%m-%d>\"" date)))))

(defun jm/dt-get-due-within (days &optional prompt)
  (jm/dt-get-within jm/dt-deadline days prompt))

(defun jm/dt-get-scheduled-within (days &optional prompt)
  (jm/dt-get-within jm/dt-scheduled days prompt))
#+end_src

** Dynamic Habits

#+begin_src emacs-lisp
(defun jm/dt-habit (habit)
  (let* ((org-date (or (org-capture-get :default-time) (current-time)))
         (today (downcase (format-time-string "%a" org-date))))
    (when (seq-contains-p (cdr habit) today)
      (car habit))))

(defun jm/dt-habits (habits)
  (let ((out-list '()))
    (dolist (habit habits out-list)
      (when-let (out (jm/dt-habit habit))
        (push out out-list)))
    (string-join out-list "\n")))
#+end_src

** Weekly Scorecard

Taken from the book /12 Week Year/, the weekly scorecard is a way to measure how well you've been acting on your plan towards your weekly goal. By seeing how effective you're execution is, you are forced to face the objective truths about your productivity.

+ [ ] My test checkbox.
+ [ ] Other checkbox.
+ [X] My test checkbox.

#+begin_src emacs-lisp
(defun jm/checkbox-checked-p (checkbox)
  (eq 'on (org-element-property :checkbox checkbox)))

(defun jm/catalog-checkboxes (buffer)
  (with-current-buffer buffer
    (let* ((filter-fn (lambda (elem) (when (org-element-property :checkbox elem) elem)))
           (elem-list (org-element-map (org-element-parse-buffer) 'item filter-fn)))
      (delq nil elem-list))))

(defun jm/score-checkboxes (buffer &optional dictionary)
  (dolist (box (jm/catalog-checkboxes buffer) dictionary)
    (with-current-buffer buffer
      (let* ((start (org-element-property :contents-begin box))
             (end (progn (goto-char start)
                         (or (- (search-forward "\n" nil t) 1) (point-max))))
             (key (buffer-substring-no-properties start end))
             (checked (if (jm/checkbox-checked-p box) 1 0))
             (pair (assoc key dictionary))
             (counts (cdr pair)))
        (if pair
            (setcdr pair (list (+ checked (car counts)) (1+ (cadr counts))))
          (push (cons key (list checked 1)) dictionary))))))

(defun jm/n-day-scorecard (n &optional start-time)
  (let ((time (or start-time (org-capture-get :default-time) (current-time)))
        (dailies-directory (expand-file-name org-roam-dailies-directory org-roam-directory))
        (dict nil))
    (dotimes (i n dict)
      (let* ((day (time-subtract time (days-to-time i)))
             (file-name (format-time-string "%Y-%m-%d.org" day))
             (file-path (expand-file-name file-name dailies-directory))
             (open (get-file-buffer file-path))
             (buffer (find-file-noselect file-path)))
        (setq dict (jm/score-checkboxes buffer dict))
        (unless open (kill-buffer buffer))))))

(defun jm/scorecard-table (tasks)
  (let* ((separator "|---|---|---|---|\n")
         (table (concat "| Task | Completed | Total | Percentage |\n" separator))
         (checked-sum 0)
         (total-sum 0))
    (dolist (box tasks table)
      (let* ((name (car box))
             (checked (cadr box))
             (total (cadr (cdr box)))
             (percentage (* (/ (float checked) total) 100)))
        (setq total-sum (+ total total-sum)
              checked-sum (+ checked checked-sum)
              table (format "%s| %s | %d | %d | %d%% |\n"
                            table name checked total percentage))))
    (format "%s%s| Average | %d | %d | %d%% |\n"
            table separator checked-sum total-sum
            (* (/ (float checked-sum) total-sum) 100))))

(defun jm/scorecard (days &optional start-time)
  (interactive)
  (jm/scorecard-table (jm/n-day-scorecard days start-time)))

(defun jm/scorecard-from-heading ()
  (interactive)
  (save-excursion
    (save-restriction
      (org-back-to-heading-or-point-min)
      (org-narrow-to-subtree)
      (let* ((score (jm/score-checkboxes (current-buffer)))
             (table (jm/scorecard-table score)))
        (org-end-of-subtree)
        (insert "\n" table "\n")))))

(jm/leader-keys
  "os" '(jm/scorecard-from-heading :which-key "Open/create yesterday's daily notes file"))
#+end_src
