From a9e5f06b03ffb9cc28ac2537ce98dc17bf08f66a Mon Sep 17 00:00:00 2001 From: Christian Cunningham Date: Thu, 13 Jun 2024 16:28:55 -0700 Subject: Agenda in Dashboard Display the agenda in the dashboard: 1. Breakdown of tasks in their categories 2. Overdue Tasks 3. Upcoming Tasks --- elchemy-dashboard.el | 136 ++++++++++++++++++++++++++++++++++++++++++++++++- elchemy-functions.el | 38 ++++++++++++++ elchemy-package-org.el | 3 ++ elchemy-user.el | 5 +- 4 files changed, 180 insertions(+), 2 deletions(-) diff --git a/elchemy-dashboard.el b/elchemy-dashboard.el index 5116807..522c546 100644 --- a/elchemy-dashboard.el +++ b/elchemy-dashboard.el @@ -34,6 +34,33 @@ (insert (buttonize name (lambda (y) (elchemy/find-file y)) path) "\n"))) (elchemy/read-alist-file (concat elchemy/elchemy-root elchemy/elchemy-projects-file))) (insert "\n")) + (ignore-errors + (setq agenda-items (mapcar #'(lambda (x) (elchemy/get-agenda-items x 3)) elchemy/dashboard-agenda-titles) + agenda-max-count (apply 'max (mapcar #'(lambda (x) (length x)) agenda-items)) + agenda-max-length (apply 'max (mapcar #'(lambda (x) (apply 'max (mapcar #'(lambda (y) (length y)) x))) agenda-items))) + (let ((start (point))) + (insert "Agenda") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (let ((start (point))) + (insert (apply 'format (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s" "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s" "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") elchemy/dashboard-agenda-titles)) + (add-text-properties start (point) + '(face (:weight bold :slant italic :foreground "red")))) + (insert "\n") + (dotimes (i agenda-max-count) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 0 agenda-items))))) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 1 agenda-items))))) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 2 agenda-items))))) + (insert "\n") + )) + (insert "\n") (let ((start (point))) (insert "Command Reference") (add-text-properties start (point) @@ -56,8 +83,115 @@ (insert "C-x C-u ~ Uppercase Region\n") (insert "C-M-n ~ Move forward one balanced expression\n") (insert "C-M-p ~ Move forward one balanced expression\n") - (setq header-line-format nil) + (insert "\n") + (insert (buttonize "Refresh" '(lambda (x) (elchemy/recreate-dashboard)))) + (insert "\n") (button-mode +1) (read-only-mode +1)))) +(defun elchemy/recreate-dashboard () + "Create the user dashboard" + (interactive) + (let ((buffer (get-buffer-create "*Dashboard*"))) + (switch-to-buffer buffer) + (read-only-mode -1) + (erase-buffer) + (when (file-exists-p (concat elchemy/elchemy-root elchemy/dashboard-splash)) + (insert-image (create-image (concat elchemy/elchemy-root elchemy/dashboard-splash) nil nil :scale 0.25)) + (insert "\n")) + (let ((start (point))) + (insert "Elchemy Dashboard") + (add-text-properties start (point) + '(face (:height 4.0)))) + (insert "\n\n") + (dotimes (i (length elchemy/dashboard/heading-buttons)) + (let* ((button (nth i elchemy/dashboard/heading-buttons)) + (title (car button)) + (callback (cdr button))) + (insert (buttonize title callback)) + (if (eq (% (+ i 1) elchemy/dashboard/heading-columns) 0) + (insert "\n") + (insert (format (concat "%-" (format "%d" (+ (- elchemy/dashboard/heading-max-length (length title)) elchemy/dashboard/heading-padding)) "s") " "))))) + (unless (eq (% (length elchemy/dashboard/heading-buttons) elchemy/dashboard/heading-columns) 0) + (insert "\n")) + (insert "\n\n") + (when (file-exists-p (concat elchemy/elchemy-root elchemy/elchemy-projects-file)) + (let ((start (point))) + (insert "Projects") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (mapcar #'(lambda (x) (let ((name (car x)) + (path (cdr x))) + (insert (buttonize name (lambda (y) (elchemy/find-file y)) path) "\n"))) + (elchemy/read-alist-file (concat elchemy/elchemy-root elchemy/elchemy-projects-file))) + (insert "\n")) + (let ((start (point))) + (insert "Agenda") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (setq agenda-items (mapcar #'(lambda (x) (elchemy/get-agenda-items x 3)) elchemy/dashboard-agenda-titles) + agenda-max-count (apply 'max (mapcar #'(lambda (x) (length x)) agenda-items)) + agenda-max-length (apply 'max (mapcar #'(lambda (x) (apply 'max (mapcar #'(lambda (y) (length y)) x))) agenda-items))) + (let ((start (point))) + (insert (apply 'format (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s" "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s" "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") elchemy/dashboard-agenda-titles)) + (add-text-properties start (point) + '(face (:weight bold :slant italic :foreground "red")))) + (insert "\n") + (dotimes (i agenda-max-count) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 0 agenda-items))))) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 1 agenda-items))))) + (insert (format + (concat "%-" (format "%d" (+ agenda-max-length elchemy/dashboard-agenda-padding)) "s") + (elchemy/replace-nil (nth i (nth 2 agenda-items))))) + (insert "\n") + ) + (insert "\n") + (let ((start (point))) + (insert "Overdue") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (insert (elchemy/format-processed-agenda (mapcar 'elchemy/process-agenda-heading (org-ql-select (org-agenda-files) `(and (todo "STRT" "WAIT" "TODO") (ts :from ,(- elchemy/schedule-lookahead) :to today)))))) + (insert "\n") + (let ((start (point))) + (insert "Upcoming") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (insert (elchemy/format-processed-agenda (mapcar 'elchemy/process-agenda-heading (org-ql-select (org-agenda-files) `(and (todo "STRT" "WAIT" "TODO") (ts :from today :to ,elchemy/schedule-lookahead)))))) + (insert "\n") + (let ((start (point))) + (insert "Command Reference") + (add-text-properties start (point) + '(face (:height 1.5)))) + (insert "\n") + (insert "C-/ ~ Undo\n") + (insert "C-x n n ~ Narrow\n") + (insert "C-x n w ~ Widen\n") + (insert "M-% ~ Query Replace\n") + (insert "C-M-s ~ Regex Search\n") + (insert "F3 ~ Record Macro\n") + (insert "F4 ~ Play Macro\n") + (insert "M-0 F4 ~ Play Macro until failure\n") + (insert "M-l ~ Lowercase following word\n") + (insert "M-u ~ Uppercase following word\n") + (insert "M-c ~ Capitalize following word\n") + (insert "M-g w ~ Jump to word\n") + (insert "M-g l ~ Jump to line\n") + (insert "C-x C-l ~ Lowercase Region\n") + (insert "C-x C-u ~ Uppercase Region\n") + (insert "C-M-n ~ Move forward one balanced expression\n") + (insert "C-M-p ~ Move forward one balanced expression\n") + (insert "\n") + (insert (buttonize "Refresh" '(lambda (x) (elchemy/recreate-dashboard)))) + (insert "\n") + (button-mode +1) + (read-only-mode +1))) + (provide 'elchemy-dashboard) diff --git a/elchemy-functions.el b/elchemy-functions.el index a5919d7..0c36ad8 100644 --- a/elchemy-functions.el +++ b/elchemy-functions.el @@ -56,4 +56,42 @@ (other-window 1) (term TERM)) +(defun elchemy/replace-nil (STRING &optional REPLACEMENT) + "Replace a nil value with an empty string" + (unless REPLACEMENT + (setq REPLACEMENT " ")) + (if STRING + STRING + REPLACEMENT)) + +(defun elchemy/get-agenda-items (TODO LEVEL) + "Get Agenda Items" + (let* ((agenda-items-raw (org-ql-select + (org-agenda-files) + `(todo ,TODO) + :sort '(date todo priority))) + (agenda-items-plist (mapcar #'(lambda (x) (cl--plist-to-alist (car (cdr x)))) agenda-items-raw)) + (agenda-items-filtered (seq-filter (lambda (x) (eq (cdr (assoc :level x)) LEVEL)) agenda-items-plist))) + (mapcar #'(lambda (x) (cdr (assoc :raw-value x))) agenda-items-filtered))) + +(defun elchemy/process-agenda-heading (heading) + "Process agenda headings + +Returns '((:raw-value . ) (:level ) (:priority . ) (:todo-keyword . ) (:scheduled . ))" + (let ((heading-alist (cl--plist-to-alist (car (cdr heading))))) + `(,(assoc :raw-value heading-alist) ,(assoc :level heading-alist) ,(assoc :priority heading-alist) (:todo-keyword . ,(substring-no-properties (cdr (assoc :todo-keyword heading-alist)))) (:scheduled . ,(cdr (assoc :raw-value (cl--plist-to-alist (nth 2 (assoc :scheduled heading-alist))))))))) + +(defun elchemy/format-priority (priority) + "Format a priority to the letter or '-' if no priority" + (if priority + priority + 45)) + +(defun elchemy/format-processed-agenda (agenda-list) + "Format a processed agenda to a string: +[PRIORITY] TODO-KEYWORD: HEADING ~ SCHEDULED" + (apply #'concat (mapcar #'(lambda (x) + (concat (format "[%c] " (elchemy/format-priority (cdr (assoc :priority x)))) (cdr (assoc :todo-keyword x)) ": " (cdr (assoc :raw-value x)) " ~ " (cdr (assoc :scheduled x)) "\n")) + agenda-list))) + (provide 'elchemy-functions) diff --git a/elchemy-package-org.el b/elchemy-package-org.el index 1aa5941..0bc3949 100644 --- a/elchemy-package-org.el +++ b/elchemy-package-org.el @@ -11,6 +11,9 @@ (global-set-key (kbd "C-c a") 'org-agenda) (global-set-key (kbd "C-c c") 'org-capture)) +(use-package org-ql + :ensure t) + (defun elchemy/hide-org-mode-stars () "Hides the section stars" (font-lock-add-keywords diff --git a/elchemy-user.el b/elchemy-user.el index fdccba8..a557804 100644 --- a/elchemy-user.el +++ b/elchemy-user.el @@ -38,7 +38,10 @@ elchemy/user/org-notes-file-name "notes.org" elchemy/user/org-hideaway t elchemy/elchemy-projects-file "projects" - elchemy/dashboard-splash "assets/splash.png") + elchemy/dashboard-splash "assets/splash.png" + elchemy/dashboard-agenda-titles '("TODO" "STRT" "WAIT") + elchemy/dashboard-agenda-padding 4 + elchemy/schedule-lookahead 7) ;; Modeline (setq-default mode-line-format '((:propertize "♥︎" face (:foreground "red")) -- cgit v1.2.1