branch: elpa/gnosis
commit 96b439a7299db71db969fcfc6b82a953fea6092a
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    [Rewrite] Move dashboard to a separate module.
---
 gnosis-dashboard.el | 617 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gnosis.el           | 565 +----------------------------------------------
 2 files changed, 619 insertions(+), 563 deletions(-)

diff --git a/gnosis-dashboard.el b/gnosis-dashboard.el
new file mode 100644
index 00000000000..ec64bde5a36
--- /dev/null
+++ b/gnosis-dashboard.el
@@ -0,0 +1,617 @@
+;;; gnosis-dashboard.el --- Dashboard Module for Gnosis  -*- lexical-binding: 
t; -*-
+
+;; Copyright (C) 2025  Thanos Apollo
+
+;; Author: Thanos Apollo <[email protected]>
+;; Keywords: extensions
+;; URL: https://thanosapollo.org/projects/gnosis
+
+;; Version: 0.0.1
+
+;; Package-Requires: ((emacs "27.2"))
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'gnosis-monkeytype)
+(require 'gnosis)
+
+(defface gnosis-face-dashboard-header
+  '((t :inherit (bold font-lock-constant-face)))
+  "Face for dashboard header.
+
+Avoid using an increased height value as this messes up with
+`gnosis-center-string' implementation"
+  :group 'gnosis)
+
+(defvar gnosis-dashboard-thema-ids nil
+  "Store thema ids for dashboard.")
+
+(defvar gnosis-dashboard-buffer-name "*Gnosis Dashboard*"
+  "Name of gnosis-dashboard buffer.")
+
+(defvar gnosis-dashboard--current
+  '(:type nil :ids nil)
+  "Current values to return after edits.")
+
+(defvar gnosis-dashboard--selected-ids nil
+  "Selected ids from the tabulated list.")
+
+(defvar gnosis-dashboard-modules
+  '(gnosis-dashboard-module-header
+    gnosis-dashboard-module-today-stats
+    gnosis-dashboard-module-average-rev))
+
+(defvar gnosis-dashboard-module-header
+  (lambda ()
+    (insert "\n"
+           (gnosis-center-string
+            (format "%s" (propertize "Gnosis Dashboard" 'face
+                                     'gnosis-face-dashboard-header))))))
+
+(defvar gnosis-dashboard-module-today-stats
+  (lambda ()
+    (insert
+     (gnosis-center-string
+      (format "\nReviewed today: %s (New: %s)"
+             (propertize
+              (number-to-string (gnosis-get-date-total-themata))
+              'face
+              'font-lock-variable-use-face)
+             (propertize
+              (number-to-string (gnosis-get-date-new-themata))
+              'face
+              'font-lock-keyword-face)))
+     "\n"
+     (gnosis-center-string
+      (format "Due themata: %s (Overdue: %s)"
+             (propertize
+              (number-to-string
+               (length (mapcar #'car (gnosis-review-get--due-themata))))
+              'face 'error)
+             (propertize
+              (number-to-string
+               (length (gnosis-review-get-overdue-themata)))
+              'face 'warning))))))
+
+(defvar gnosis-dashboard-module-average-rev
+  (lambda ()
+    (insert
+     (gnosis-center-string
+      (format "Daily Average: %s"
+             (propertize
+              (format "%.2f" (gnosis-calculate-average-daily-reviews))
+              'face 'font-lock-type-face)))
+     "\n"
+     (gnosis-center-string
+      (format "Current streak: %s day(s)"
+             (propertize
+              (gnosis-dashboard--streak
+               (gnosis-select 'date 'activity-log '(> reviewed-total 0) t))
+              'face 'success))))))
+
+(defvar gnosis-dashboard-module-monkeytype
+  (lambda ()
+    (and gnosis-monkeytype-wpm-result
+        (insert
+         (gnosis-center-string
+          (format "Latest WPM: %.2f" gnosis-monkeytype-wpm-result))))))
+
+(defun gnosis-dashboard-return (&optional current-values)
+  "Return to dashboard for CURRENT-VALUES."
+  (interactive)
+  (let* ((current-values (or current-values gnosis-dashboard--current))
+        (type (plist-get current-values :type))
+        (ids (plist-get current-values :ids)))
+    (cond ((eq type 'themata)
+          (gnosis-dashboard-output-themata ids))
+         ((eq type 'decks)
+          (gnosis-dashboard-output-decks))
+         ((eq type 'tags)
+          (gnosis-dashboard-output-tags))
+         ((eq type 'history)
+          (gnosis-dashboard-history)))))
+
+(defun gnosis-dashboard--streak (dates &optional num date)
+  "Return current review streak number as a string.
+
+DATES: Dates in the activity log, a list of dates in (YYYY MM DD).
+NUM: Streak number.
+DATE: Integer, used with `gnosis-algorithm-date' to get previous dates."
+  (let ((num (or num 0))
+       (date (or date -1)))
+    (cond ((> num 666)
+          "+666") ;; do not go over 666, avoiding `max-lisp-eval-depth'
+         ((member (gnosis-algorithm-date date) dates)
+          (gnosis-dashboard--streak dates (cl-incf num) (- date 1)))
+         (t (number-to-string (if (member (gnosis-algorithm-date) dates)
+                                  (+ 1 num)
+                                num))))))
+
+(defun gnosis-dashboard-output-average-rev ()
+  "Output the average daily themata reviewed as a string for the dashboard."
+  (format "%.2f" (gnosis-calculate-average-daily-reviews)))
+
+(defun gnosis-dashboard-edit-thema ()
+  "Edit thema with ID."
+  (interactive)
+  (let ((id (tabulated-list-get-id)))
+    (gnosis-edit-thema id)))
+
+(defun gnosis-dashboard-suspend-thema ()
+  "Suspend thema."
+  (interactive nil gnosis-dashboard-themata-mode)
+  (let ((current-line (line-number-at-pos)))
+    (gnosis-toggle-suspend-themata
+     (or gnosis-dashboard--selected-ids (list (tabulated-list-get-id))))
+    (gnosis-dashboard-output-themata gnosis-dashboard-thema-ids)
+    (revert-buffer t t t)
+    (forward-line (- current-line 1))))
+
+(defun gnosis-dashboard-delete ()
+  "Delete thema."
+  (interactive)
+  (let ((current-line (line-number-at-pos)))
+    (if gnosis-dashboard--selected-ids
+       (gnosis-dashboard-marked-delete)
+      (gnosis-delete-thema (tabulated-list-get-id))
+      (gnosis-dashboard-output-themata gnosis-dashboard-thema-ids)
+      (revert-buffer t t t))
+    (forward-line (- current-line 1))))
+
+(defun gnosis-dashboard-search-thema (&optional str)
+  "Search for themata with STR."
+  (interactive)
+  (gnosis-dashboard-output-themata
+   (gnosis-collect-thema-ids :query (or str (read-string "Search for thema: 
")))))
+
+(defvar-keymap gnosis-dashboard-themata-mode-map
+  :doc "Keymap for themata dashboard."
+  "q" #'gnosis-dashboard
+  "e" #'gnosis-dashboard-edit-thema
+  "s" #'gnosis-dashboard-suspend-thema
+  "SPC" #'gnosis-dashboard-search-thema
+  "a" #'gnosis-add-thema
+  "r" #'gnosis-dashboard-return
+  "g" #'gnosis-dashboard-return
+  "d" #'gnosis-dashboard-delete
+  "m" #'gnosis-dashboard-mark-toggle
+  "M" #'gnosis-dashboard-mark-all
+  "u" #'gnosis-dashboard-mark-toggle
+  "U" #'gnosis-dashboard-unmark-all)
+
+(define-minor-mode gnosis-dashboard-themata-mode
+  "Minor mode for gnosis dashboard themata output."
+  :keymap gnosis-dashboard-themata-mode-map
+  (gnosis-dashboard-decks-mode -1)
+  (gnosis-dashboard-tags-mode -1))
+
+(defun gnosis-dashboard--output-themata (thema-ids)
+  "Output tabulated-list format for THEMA-IDS."
+  (cl-assert (listp thema-ids))
+  (let ((entries (emacsql gnosis-db
+                         `[:select
+                           [themata:id themata:keimenon themata:hypothesis 
themata:answer
+                                     themata:tags themata:type 
review-log:suspend]
+                           :from themata
+                           :join review-log :on (= themata:id review-log:id)
+                           :where (in themata:id ,(vconcat thema-ids))])))
+    (cl-loop for sublist in entries
+             collect
+            (list (car sublist)
+                   (vconcat
+                   (cl-loop for item in (cdr sublist)
+                            if (listp item)
+                            collect (mapconcat (lambda (x) (format "%s" x)) 
item ",")
+                            else
+                            collect
+                            (replace-regexp-in-string "\n" " " (format "%s" 
item))))))))
+
+(defun gnosis-dashboard-output-themata (thema-ids)
+  "Return THEMA-IDS contents on gnosis dashboard."
+  (cl-assert (listp thema-ids) t "`thema-ids' must be a list of thema ids.")
+  (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
+  (gnosis-dashboard-enable-mode)
+  (gnosis-dashboard-themata-mode)
+  (setf tabulated-list-format `[("Keimenon" ,(/ (window-width) 4) t)
+                                ("Hypothesis" ,(/ (window-width) 6) t)
+                                ("Answer" ,(/ (window-width) 6) t)
+                                ("Tags" ,(/ (window-width) 5) t)
+                                ("Type" ,(/ (window-width) 10) t)
+                                ("Suspend" ,(/ (window-width) 6) t)]
+        gnosis-dashboard-thema-ids thema-ids
+        tabulated-list-entries nil)
+  (make-local-variable 'tabulated-list-entries)
+  (tabulated-list-init-header)
+  (let ((inhibit-read-only t)
+       (entries (gnosis-dashboard--output-themata thema-ids)))
+    (erase-buffer)
+    (insert (format "Loading %s themata..." (length thema-ids)))
+    (setq tabulated-list-entries entries)
+    (tabulated-list-print t)
+    (setf gnosis-dashboard--current
+         `(:type themata :ids ,thema-ids))))
+
+(defun gnosis-dashboard-deck-thema-count (id)
+  "Return total thema count for deck with ID."
+  (let ((thema-count (length (gnosis-select 'id 'themata `(= deck-id ,id) t))))
+    (when (gnosis-select 'id 'decks `(= id ,id))
+      (list (number-to-string thema-count)))))
+
+(defun gnosis-dashboard-output-tag (tag)
+  "Output TAG name and total themata."
+  (let ((themata (gnosis-get-tag-themata tag)))
+    `(,tag ,(number-to-string (length themata)))))
+
+(defun gnosis-dashboard-sort-total-themata (entry1 entry2)
+  "Sort function for the total themata column, for ENTRY1 and ENTRY2."
+  (let ((total1 (string-to-number (elt (cadr entry1) 1)))
+        (total2 (string-to-number (elt (cadr entry2) 1))))
+    (< total1 total2)))
+
+(defun gnosis-dashboard-rename-tag ()
+  "Rename TAG to NEW-TAG."
+  (interactive)
+  (let ((current-line (line-number-at-pos)))
+    (gnosis-tag-rename (tabulated-list-get-id))
+    (gnosis-dashboard-output-tags)
+    (forward-line (- current-line 1))))
+
+(defun gnosis-dashboard-delete-tag (&optional tag)
+  "Rename TAG to NEW-TAG."
+  (interactive)
+  (let ((tag (or tag (tabulated-list-get-id))))
+    (when (y-or-n-p (format "Delete tag %s?"
+                           (propertize tag 'face 'font-lock-keyword-face)))
+      (cl-loop for thema in (gnosis-get-tag-themata tag)
+              do (let* ((tags (car (gnosis-select '[tags] 'themata `(= id 
,thema) t)))
+                        (new-tags (remove tag tags)))
+                   (gnosis-update 'themata `(= tags ',new-tags) `(= id 
,thema))))
+      ;; Update tags in database
+      (gnosis-tags-refresh)
+      ;; Output tags anew
+      (gnosis-dashboard-output-tags))))
+
+
+(defun gnosis-dashboard-rename-deck (&optional deck-id new-name)
+  "Rename deck where DECK-ID with NEW-NAME."
+  (interactive)
+  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id))))
+       (new-name (or new-name (read-string "New deck name: "))))
+    (gnosis-update 'decks `(= name ,new-name) `(= id ,deck-id))
+    (gnosis-dashboard-output-decks)))
+
+(defun gnosis-dashboard-suspend-tag (&optional tag)
+  "Suspend themata of TAG."
+  (interactive)
+  (let* ((tag (or tag (tabulated-list-get-id)))
+        (themata (gnosis-get-tag-themata tag))
+        (suspend (if current-prefix-arg 0 1))
+        (confirm-msg (y-or-n-p
+                      (if (= suspend 0)
+                          "Unsuspend all themata for tag? "
+                        "Suspend all themata for tag?"))))
+    (when confirm-msg
+      (emacsql gnosis-db
+              `[:update review-log :set (= suspend ,suspend) :where
+                        (in id ,(vconcat themata))])
+      (if (= suspend 0)
+         (message "Unsuspended %s themata" (length themata))
+       (message "Suspended %s themata" (length themata))))))
+
+(defun gnosis-dashboard-tag-view-themata (&optional tag)
+  "View themata for TAG."
+  (interactive)
+  (let ((tag (or tag (tabulated-list-get-id))))
+    (gnosis-dashboard-output-themata (gnosis-get-tag-themata tag))))
+
+(defvar-keymap gnosis-dashboard-tags-mode-map
+  "RET" #'gnosis-dashboard-tag-view-themata
+  "e" #'gnosis-dashboard-rename-tag
+  "q" #'gnosis-dashboard
+  "s" #'gnosis-dashboard-suspend-tag
+  "r" #'gnosis-dashboard-rename-tag
+  "d" #'gnosis-dashboard-delete-tag
+  "g" #'gnosis-dashboard-return)
+
+(define-minor-mode gnosis-dashboard-tags-mode
+  "Mode for dashboard output of tags."
+  :keymap gnosis-dashboard-tags-mode-map)
+
+(defun gnosis-dashboard-output-tags (&optional tags)
+  "Format gnosis dashboard with output of TAGS."
+  (gnosis-tags-refresh) ;; Refresh tags
+  (let ((tags (or tags (gnosis-get-tags--unique))))
+    (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
+    (gnosis-dashboard-enable-mode)
+    (gnosis-dashboard-tags-mode)
+    (setf gnosis-dashboard--current '(:type 'tags))
+    (setq tabulated-list-format [("Name" 35 t)
+                                 ("Total Themata" 10 
gnosis-dashboard-sort-total-themata)])
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          (cl-loop for tag in tags
+                   collect (list (car (gnosis-dashboard-output-tag tag))
+                                 (vconcat (gnosis-dashboard-output-tag tag)))))
+    (tabulated-list-print t)))
+
+(defun gnosis-dashboard-output-deck (id)
+  "Output contents from deck ID, formatted for gnosis dashboard."
+  (let* ((deck-name (gnosis-select 'name 'decks `(= id ,id) t))
+         (thema-count (gnosis-dashboard-deck-thema-count id))
+         (combined-data (append deck-name (mapcar #'string-to-number 
thema-count))))
+    (mapcar (lambda (item) (format "%s" item))
+            (seq-filter (lambda (item)
+                         (not (and (vectorp item) (seq-empty-p item))))
+                       combined-data))))
+
+(defvar-keymap gnosis-dashboard-decks-mode-map
+  "e" #'gnosis-dashboard-rename-deck
+  "r" #'gnosis-dashboard-rename-deck
+  "q" #'gnosis-dashboard
+  "a" #'gnosis-dashboard-decks-add
+  "s" #'gnosis-dashboard-decks-suspend-deck
+  "d" #'gnosis-dashboard-decks-delete
+  "RET" #'gnosis-dashboard-decks-view-deck)
+
+(define-minor-mode gnosis-dashboard-decks-mode
+  "Minor mode for deck output."
+  :keymap gnosis-dashboard-decks-mode-map)
+
+(defun gnosis-dashboard-output-decks ()
+  "Return deck contents for gnosis dashboard."
+  (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
+  (gnosis-dashboard-enable-mode)
+  (gnosis-dashboard-decks-mode)
+  (setq tabulated-list-format [("Name" 15 t)
+                              ("Total Themata" 10 
gnosis-dashboard-sort-total-themata)])
+  (tabulated-list-init-header)
+  (setq tabulated-list-entries
+       (cl-loop for id in (gnosis-select 'id 'decks nil t)
+                for output = (gnosis-dashboard-output-deck id)
+                when output
+                collect (list (number-to-string id) (vconcat output))))
+  (tabulated-list-print t)
+  (setf gnosis-dashboard--current `(:type decks :ids ,(gnosis-select 'id 
'decks nil t))))
+
+(defun gnosis-dashboard-decks-add ()
+  "Add deck & refresh."
+  (interactive)
+  (gnosis-add-deck (read-string "Deck name: "))
+  (gnosis-dashboard-output-decks)
+  (revert-buffer t t t))
+
+(defun gnosis-dashboard-decks-suspend-deck (&optional deck-id)
+  "Suspend themata for DECK-ID.
+
+When called with called with a prefix, unsuspend all themata of deck."
+  (interactive)
+  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+    (gnosis-suspend-deck deck-id)
+    (gnosis-dashboard-output-decks)
+    (revert-buffer t t t)))
+
+(defun gnosis-dashboard-decks-delete (&optional deck-id)
+  "Delete DECK-ID."
+  (interactive)
+  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+    (gnosis-delete-deck deck-id)
+    (gnosis-dashboard-output-decks)
+    (revert-buffer t t t)))
+
+(defun gnosis-dashboard-decks-view-deck (&optional deck-id)
+  "View themata of DECK-ID."
+  (interactive)
+  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+    (gnosis-dashboard-output-themata (gnosis-collect-thema-ids :deck 
deck-id))))
+
+(defun gnosis-dashboard-history (&optional history)
+  "Display review HISTORY."
+  (interactive)
+  (let* ((history (or history
+                     (gnosis-select '[date reviewed-total reviewed-new]
+                                    'activity-log)))
+        (buffer (get-buffer-create "*Gnosis History*")))
+    (with-current-buffer buffer
+      (let ((inhibit-read-only t))
+       (erase-buffer))
+      (tabulated-list-mode)
+      (setq tabulated-list-format
+            `[("Date" ,(/ (window-width) 6) t)
+              ("Total Reviews" ,(/ (window-width) 6) 
gnosis-dashboard-sort-total-themata)
+              ("New" ,(/ (window-width) 6) 
gnosis-dashboard-sort-total-themata)])
+      (make-local-variable 'tabulated-list-entries)
+      ;; Sort for date
+      (setq tabulated-list-sort-key (cons "Date" t))
+      (setq tabulated-list-entries
+            (cl-loop for entry in history
+                     collect (list (car entry)
+                                   (vector (propertize
+                                           (format "%04d/%02d/%02d"
+                                                   (nth 0 (car entry))
+                                                   (nth 1 (car entry))
+                                                   (nth 2 (car entry)))
+                                           'face 'org-date)
+                                           (number-to-string (cadr entry))
+                                           (number-to-string (caddr entry))))))
+      (tabulated-list-init-header)
+      (tabulated-list-print t)
+      (setq gnosis-dashboard--current
+           '(:type history)))
+    (pop-to-buffer buffer)))
+
+(defvar-keymap gnosis-dashboard-mode-map
+  :doc "gnosis-dashboard keymap"
+  "q" #'quit-window
+  "h" #'gnosis-dashboard-menu
+  "H" #'gnosis-dashboard-history
+  "r" #'gnosis-review
+  "a" #'gnosis-add-thema
+  "A" #'gnosis-add-deck
+  "s" #'gnosis-dashboard-suffix-query
+  "n" #'(lambda () (interactive) (gnosis-dashboard-output-themata 
(gnosis-collect-thema-ids)))
+  "d" #'gnosis-dashboard-suffix-decks
+  "t" #'(lambda () (interactive) (gnosis-dashboard-output-tags))
+  "m" #'gnosis-monkeytype-start)
+
+(define-derived-mode gnosis-dashboard-mode tabulated-list-mode "Gnosis 
Dashboard"
+  "Major mode for displaying Gnosis dashboard."
+  :keymap gnosis-dashboard-mode-map
+  :interactive nil
+  (setq-local header-line-format nil)
+  (setq tabulated-list-padding 2
+       tabulated-list-sort-key nil
+       gnosis-dashboard--selected-ids nil)
+  (display-line-numbers-mode 0))
+
+(defun gnosis-dashboard-enable-mode ()
+  "Enable `gnosis-dashboard-mode'."
+  (when (and (string= (buffer-name) gnosis-dashboard-buffer-name)
+            (not (eq major-mode 'gnosis-dashboard-mode)))
+    (gnosis-dashboard-mode)))
+
+(cl-defun gnosis-dashboard--search (&optional dashboard-type (thema-ids nil))
+  "Display gnosis dashboard.
+
+THEMA-IDS: List of thema ids to display on dashboard.  When nil, prompt
+for dashboard type.
+
+DASHBOARD-TYPE: either Themata or Decks to display the respective dashboard."
+  (interactive)
+  (let ((dashboard-type (or dashboard-type
+                           (cadr (read-multiple-choice
+                                  "Display dashboard for:"
+                                  '((?n "themata")
+                                    (?d "decks")
+                                    (?t "tags")
+                                    (?s "search")))))))
+    (if thema-ids (gnosis-dashboard-output-themata thema-ids)
+      (pcase dashboard-type
+       ("themata" (gnosis-dashboard-output-themata (gnosis-collect-thema-ids)))
+       ("decks" (gnosis-dashboard-output-decks))
+       ("tags"  (gnosis-dashboard-output-themata (gnosis-collect-thema-ids 
:tags t)))
+       ("search" (gnosis-dashboard-search-thema))))
+    (tabulated-list-print t)))
+
+(defun gnosis-dashboard-mark-toggle ()
+  "Toggle mark on the current item in the tabulated-list."
+  (interactive)
+  (let ((inhibit-read-only t)
+        (entry (tabulated-list-get-entry))
+       (id (tabulated-list-get-id)))
+    (if entry
+        (let ((beg (line-beginning-position))
+              (end (line-end-position))
+              (overlays (overlays-in (line-beginning-position) 
(line-end-position))))
+          (if (cl-some (lambda (ov) (overlay-get ov 'gnosis-mark)) overlays)
+              (progn
+                (remove-overlays beg end 'gnosis-mark t)
+               (setq gnosis-dashboard--selected-ids
+                     (remove id gnosis-dashboard--selected-ids)))
+            (let ((ov (make-overlay beg end)))
+             (unless (member id gnosis-dashboard--selected-ids)
+               (setf gnosis-dashboard--selected-ids
+                     (cons id gnosis-dashboard--selected-ids)))
+              (overlay-put ov 'face 'highlight)
+              (overlay-put ov 'gnosis-mark t)))
+         (forward-line))
+      (message "No entry at point"))))
+
+(defun gnosis-dashboard-unmark-all ()
+  "Unmark all items in the tabulated-list."
+  (interactive)
+  (let ((inhibit-read-only t))
+    (setq gnosis-dashboard--selected-ids nil)
+    (remove-overlays nil nil 'gnosis-mark t)
+    (message "All items unmarked")))
+
+(defun gnosis-dashboard-mark-all ()
+  "Mark all items in the tabulated-list buffer and collect their IDs."
+  (interactive)
+  (when (derived-mode-p 'tabulated-list-mode)
+    (let ((inhibit-read-only t))
+      ;; Clear existing marks
+      (remove-overlays (point-min) (point-max) 'gnosis-mark t)
+      ;; Apply overlay to the entire buffer at once
+      (let ((ov (make-overlay (point-min) (point-max))))
+        (overlay-put ov 'face 'highlight)
+        (overlay-put ov 'gnosis-mark t))
+      ;; Set selected IDs
+      (setq gnosis-dashboard--selected-ids gnosis-dashboard-thema-ids)
+      (message "Marked %d items" (count-lines (point-min) (point-max))))))
+
+(defun gnosis-dashboard-marked-delete ()
+  "Delete marked thema entries."
+  (interactive)
+  (when (y-or-n-p "Delete selected themata?")
+    (cl-loop for thema in gnosis-dashboard--selected-ids
+            do (gnosis-delete-thema thema t))
+    (gnosis-dashboard-return)))
+
+(defun gnosis-dashboard-marked-suspend ()
+  "Suspend marked thema entries."
+  (interactive)
+  (when (y-or-n-p "Toggle SUSPEND on selected themata?")
+    (gnosis-toggle-suspend-themata gnosis-dashboard--selected-ids nil)
+    (gnosis-dashboard-return)))
+
+(transient-define-suffix gnosis-dashboard-suffix-query (query)
+  "Search for thema content for QUERY."
+  (interactive "sSearch for thema content: ")
+  (gnosis-dashboard-output-themata (gnosis-collect-thema-ids :query query)))
+
+(transient-define-suffix gnosis-dashboard-suffix-decks ()
+  (interactive)
+  (gnosis-dashboard-output-decks))
+
+(transient-define-prefix gnosis-dashboard-menu ()
+  "Transient buffer for gnosis dashboard interactions."
+  [["Actions"
+    ("r" "Review" gnosis-review)
+    ("a" "Add thema" gnosis-add-thema)
+    ("A" "Add deck" gnosis-add-deck)
+    ("q" "Quit" quit-window)
+    "\n"]
+   ["Themata"
+    ("s" "Search" gnosis-dashboard-suffix-query)
+    ("n" "Themata" (lambda () (interactive)
+                  (gnosis-dashboard-output-themata
+                   (gnosis-collect-thema-ids))))
+    ("d" "Decks" gnosis-dashboard-suffix-decks)
+    ("t" "Tags" (lambda () (interactive)
+                 (gnosis-dashboard-output-tags)))]
+   ["History"
+    ("H" "View Review History" gnosis-dashboard-history)]])
+
+;;;###autoload
+(defun gnosis-dashboard ()
+  "Launch gnosis dashboard."
+  (interactive)
+  (let* ((buffer (get-buffer-create gnosis-dashboard-buffer-name))
+        (inhibit-read-only t))
+    (with-current-buffer buffer
+      (erase-buffer)
+      (gnosis-dashboard-mode)
+      (dolist (module gnosis-dashboard-modules)
+       (funcall (symbol-value module))
+       (gnosis-insert-separator)))
+    (pop-to-buffer-same-window buffer)
+    (goto-char (point-min))
+    (gnosis-dashboard-enable-mode)))
+
+(provide 'gnosis-dashboard)
+;;; gnosis-dashboard.el ends here
diff --git a/gnosis.el b/gnosis.el
index bbe756383bb..dd9b246c70e 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -145,17 +145,12 @@ When nil, review new themata last."
   '((t :inherit bold))
   "Face for next review.")
 
-(defface gnosis-face-dashboard-header
-  '((t :inherit (bold font-lock-constant-face)))
-  "Face for dashboard header.
-
-Avoid using an increased height value as this messes up with
-`gnosis-center-string' implementation")
-
 (defconst gnosis-db
   (emacsql-sqlite-open (expand-file-name "gnosis.db" gnosis-dir))
   "Gnosis database.")
 
+(autoload 'gnosis-dashboard "gnosis-dashboard" nil t)
+
 (defvar gnosis-testing nil
   "Change this to non-nil when running manual tests.")
 
@@ -2380,562 +2375,6 @@ Return thema ids for themata that match QUERY."
 
 (gnosis-db-init)
 
-;; Dashboard
-;;;;;;;;;;;;
-
-(defvar gnosis-dashboard-thema-ids nil
-  "Store thema ids for dashboard.")
-
-(defvar gnosis-dashboard-buffer-name "*Gnosis Dashboard*"
-  "Name of gnosis-dashboard buffer.")
-
-(defvar gnosis-dashboard--current
-  '(:type nil :ids nil)
-  "Current values to return after edits.")
-
-(defvar gnosis-dashboard--selected-ids nil
-  "Selected ids from the tabulated list.")
-
-
-(defun gnosis-dashboard-return (&optional current-values)
-  "Return to dashboard for CURRENT-VALUES."
-  (interactive)
-  (let* ((current-values (or current-values gnosis-dashboard--current))
-        (type (plist-get current-values :type))
-        (ids (plist-get current-values :ids)))
-    (cond ((eq type 'themata)
-          (gnosis-dashboard-output-themata ids))
-         ((eq type 'decks)
-          (gnosis-dashboard-output-decks))
-         ((eq type 'tags)
-          (gnosis-dashboard-output-tags))
-         ((eq type 'history)
-          (gnosis-dashboard-history)))))
-
-(defun gnosis-dashboard--streak (dates &optional num date)
-  "Return current review streak number as a string.
-
-DATES: Dates in the activity log, a list of dates in (YYYY MM DD).
-NUM: Streak number.
-DATE: Integer, used with `gnosis-algorithm-date' to get previous dates."
-  (let ((num (or num 0))
-       (date (or date -1)))
-    (cond ((> num 666)
-          "+666") ;; do not go over 666, avoiding `max-lisp-eval-depth'
-         ((member (gnosis-algorithm-date date) dates)
-          (gnosis-dashboard--streak dates (cl-incf num) (- date 1)))
-         (t (number-to-string (if (member (gnosis-algorithm-date) dates)
-                                  (+ 1 num)
-                                num))))))
-
-(defun gnosis-dashboard-output-average-rev ()
-  "Output the average daily themata reviewed as a string for the dashboard."
-  (format "%.2f" (gnosis-calculate-average-daily-reviews)))
-
-(defun gnosis-dashboard-edit-thema ()
-  "Edit thema with ID."
-  (interactive)
-  (let ((id (tabulated-list-get-id)))
-    (gnosis-edit-thema id)))
-
-(defun gnosis-dashboard-suspend-thema ()
-  "Suspend thema."
-  (interactive nil gnosis-dashboard-themata-mode)
-  (let ((current-line (line-number-at-pos)))
-    (gnosis-toggle-suspend-themata
-     (or gnosis-dashboard--selected-ids (list (tabulated-list-get-id))))
-    (gnosis-dashboard-output-themata gnosis-dashboard-thema-ids)
-    (revert-buffer t t t)
-    (forward-line (- current-line 1))))
-
-(defun gnosis-dashboard-delete ()
-  "Delete thema."
-  (interactive)
-  (let ((current-line (line-number-at-pos)))
-    (if gnosis-dashboard--selected-ids
-       (gnosis-dashboard-marked-delete)
-      (gnosis-delete-thema (tabulated-list-get-id))
-      (gnosis-dashboard-output-themata gnosis-dashboard-thema-ids)
-      (revert-buffer t t t))
-    (forward-line (- current-line 1))))
-
-(defun gnosis-dashboard-search-thema (&optional str)
-  "Search for themata with STR."
-  (interactive)
-  (gnosis-dashboard-output-themata
-   (gnosis-collect-thema-ids :query (or str (read-string "Search for thema: 
")))))
-
-(defvar-keymap gnosis-dashboard-themata-mode-map
-  :doc "Keymap for themata dashboard."
-  "q" #'gnosis-dashboard
-  "e" #'gnosis-dashboard-edit-thema
-  "s" #'gnosis-dashboard-suspend-thema
-  "SPC" #'gnosis-dashboard-search-thema
-  "a" #'gnosis-add-thema
-  "r" #'gnosis-dashboard-return
-  "g" #'gnosis-dashboard-return
-  "d" #'gnosis-dashboard-delete
-  "m" #'gnosis-dashboard-mark-toggle
-  "M" #'gnosis-dashboard-mark-all
-  "u" #'gnosis-dashboard-mark-toggle
-  "U" #'gnosis-dashboard-unmark-all)
-
-(define-minor-mode gnosis-dashboard-themata-mode
-  "Minor mode for gnosis dashboard themata output."
-  :keymap gnosis-dashboard-themata-mode-map
-  (gnosis-dashboard-decks-mode -1)
-  (gnosis-dashboard-tags-mode -1))
-
-(defun gnosis-dashboard--output-themata (thema-ids)
-  "Output tabulated-list format for THEMA-IDS."
-  (cl-assert (listp thema-ids))
-  (let ((entries (emacsql gnosis-db
-                         `[:select
-                           [themata:id themata:keimenon themata:hypothesis 
themata:answer
-                                     themata:tags themata:type 
review-log:suspend]
-                           :from themata
-                           :join review-log :on (= themata:id review-log:id)
-                           :where (in themata:id ,(vconcat thema-ids))])))
-    (cl-loop for sublist in entries
-             collect
-            (list (car sublist)
-                   (vconcat
-                   (cl-loop for item in (cdr sublist)
-                            if (listp item)
-                            collect (mapconcat (lambda (x) (format "%s" x)) 
item ",")
-                            else
-                            collect
-                            (replace-regexp-in-string "\n" " " (format "%s" 
item))))))))
-
-(defun gnosis-dashboard-output-themata (thema-ids)
-  "Return THEMA-IDS contents on gnosis dashboard."
-  (cl-assert (listp thema-ids) t "`thema-ids' must be a list of thema ids.")
-  (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
-  (gnosis-dashboard-enable-mode)
-  (gnosis-dashboard-themata-mode)
-  (setf tabulated-list-format `[("Keimenon" ,(/ (window-width) 4) t)
-                                ("Hypothesis" ,(/ (window-width) 6) t)
-                                ("Answer" ,(/ (window-width) 6) t)
-                                ("Tags" ,(/ (window-width) 5) t)
-                                ("Type" ,(/ (window-width) 10) t)
-                                ("Suspend" ,(/ (window-width) 6) t)]
-        gnosis-dashboard-thema-ids thema-ids
-        tabulated-list-entries nil)
-  (make-local-variable 'tabulated-list-entries)
-  (tabulated-list-init-header)
-  (let ((inhibit-read-only t)
-       (entries (gnosis-dashboard--output-themata thema-ids)))
-    (erase-buffer)
-    (insert (format "Loading %s themata..." (length thema-ids)))
-    (setq tabulated-list-entries entries)
-    (tabulated-list-print t)
-    (setf gnosis-dashboard--current
-         `(:type themata :ids ,thema-ids))))
-
-(defun gnosis-dashboard-deck-thema-count (id)
-  "Return total thema count for deck with ID."
-  (let ((thema-count (length (gnosis-select 'id 'themata `(= deck-id ,id) t))))
-    (when (gnosis-select 'id 'decks `(= id ,id))
-      (list (number-to-string thema-count)))))
-
-(defun gnosis-dashboard-output-tag (tag)
-  "Output TAG name and total themata."
-  (let ((themata (gnosis-get-tag-themata tag)))
-    `(,tag ,(number-to-string (length themata)))))
-
-(defun gnosis-dashboard-sort-total-themata (entry1 entry2)
-  "Sort function for the total themata column, for ENTRY1 and ENTRY2."
-  (let ((total1 (string-to-number (elt (cadr entry1) 1)))
-        (total2 (string-to-number (elt (cadr entry2) 1))))
-    (< total1 total2)))
-
-(defun gnosis-dashboard-rename-tag ()
-  "Rename TAG to NEW-TAG."
-  (interactive)
-  (let ((current-line (line-number-at-pos)))
-    (gnosis-tag-rename (tabulated-list-get-id))
-    (gnosis-dashboard-output-tags)
-    (forward-line (- current-line 1))))
-
-(defun gnosis-dashboard-delete-tag (&optional tag)
-  "Rename TAG to NEW-TAG."
-  (interactive)
-  (let ((tag (or tag (tabulated-list-get-id))))
-    (when (y-or-n-p (format "Delete tag %s?"
-                           (propertize tag 'face 'font-lock-keyword-face)))
-      (cl-loop for thema in (gnosis-get-tag-themata tag)
-              do (let* ((tags (car (gnosis-select '[tags] 'themata `(= id 
,thema) t)))
-                        (new-tags (remove tag tags)))
-                   (gnosis-update 'themata `(= tags ',new-tags) `(= id 
,thema))))
-      ;; Update tags in database
-      (gnosis-tags-refresh)
-      ;; Output tags anew
-      (gnosis-dashboard-output-tags))))
-
-
-(defun gnosis-dashboard-rename-deck (&optional deck-id new-name)
-  "Rename deck where DECK-ID with NEW-NAME."
-  (interactive)
-  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id))))
-       (new-name (or new-name (read-string "New deck name: "))))
-    (gnosis-update 'decks `(= name ,new-name) `(= id ,deck-id))
-    (gnosis-dashboard-output-decks)))
-
-(defun gnosis-dashboard-suspend-tag (&optional tag)
-  "Suspend themata of TAG."
-  (interactive)
-  (let* ((tag (or tag (tabulated-list-get-id)))
-        (themata (gnosis-get-tag-themata tag))
-        (suspend (if current-prefix-arg 0 1))
-        (confirm-msg (y-or-n-p
-                      (if (= suspend 0)
-                          "Unsuspend all themata for tag? "
-                        "Suspend all themata for tag?"))))
-    (when confirm-msg
-      (emacsql gnosis-db
-              `[:update review-log :set (= suspend ,suspend) :where
-                        (in id ,(vconcat themata))])
-      (if (= suspend 0)
-         (message "Unsuspended %s themata" (length themata))
-       (message "Suspended %s themata" (length themata))))))
-
-(defun gnosis-dashboard-tag-view-themata (&optional tag)
-  "View themata for TAG."
-  (interactive)
-  (let ((tag (or tag (tabulated-list-get-id))))
-    (gnosis-dashboard-output-themata (gnosis-get-tag-themata tag))))
-
-(defvar-keymap gnosis-dashboard-tags-mode-map
-  "RET" #'gnosis-dashboard-tag-view-themata
-  "e" #'gnosis-dashboard-rename-tag
-  "q" #'gnosis-dashboard
-  "s" #'gnosis-dashboard-suspend-tag
-  "r" #'gnosis-dashboard-rename-tag
-  "d" #'gnosis-dashboard-delete-tag
-  "g" #'gnosis-dashboard-return)
-
-(define-minor-mode gnosis-dashboard-tags-mode
-  "Mode for dashboard output of tags."
-  :keymap gnosis-dashboard-tags-mode-map)
-
-(defun gnosis-dashboard-output-tags (&optional tags)
-  "Format gnosis dashboard with output of TAGS."
-  (gnosis-tags-refresh) ;; Refresh tags
-  (let ((tags (or tags (gnosis-get-tags--unique))))
-    (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
-    (gnosis-dashboard-enable-mode)
-    (gnosis-dashboard-tags-mode)
-    (setf gnosis-dashboard--current '(:type 'tags))
-    (setq tabulated-list-format [("Name" 35 t)
-                                 ("Total Themata" 10 
gnosis-dashboard-sort-total-themata)])
-    (tabulated-list-init-header)
-    (setq tabulated-list-entries
-          (cl-loop for tag in tags
-                   collect (list (car (gnosis-dashboard-output-tag tag))
-                                 (vconcat (gnosis-dashboard-output-tag tag)))))
-    (tabulated-list-print t)))
-
-(defun gnosis-dashboard-output-deck (id)
-  "Output contents from deck ID, formatted for gnosis dashboard."
-  (let* ((deck-name (gnosis-select 'name 'decks `(= id ,id) t))
-         (thema-count (gnosis-dashboard-deck-thema-count id))
-         (combined-data (append deck-name (mapcar #'string-to-number 
thema-count))))
-    (mapcar (lambda (item) (format "%s" item))
-            (seq-filter (lambda (item)
-                         (not (and (vectorp item) (seq-empty-p item))))
-                       combined-data))))
-
-(defvar-keymap gnosis-dashboard-decks-mode-map
-  "e" #'gnosis-dashboard-rename-deck
-  "r" #'gnosis-dashboard-rename-deck
-  "q" #'gnosis-dashboard
-  "a" #'gnosis-dashboard-decks-add
-  "s" #'gnosis-dashboard-decks-suspend-deck
-  "d" #'gnosis-dashboard-decks-delete
-  "RET" #'gnosis-dashboard-decks-view-deck)
-
-(define-minor-mode gnosis-dashboard-decks-mode
-  "Minor mode for deck output."
-  :keymap gnosis-dashboard-decks-mode-map)
-
-(defun gnosis-dashboard-output-decks ()
-  "Return deck contents for gnosis dashboard."
-  (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
-  (gnosis-dashboard-enable-mode)
-  (gnosis-dashboard-decks-mode)
-  (setq tabulated-list-format [("Name" 15 t)
-                              ("Total Themata" 10 
gnosis-dashboard-sort-total-themata)])
-  (tabulated-list-init-header)
-  (setq tabulated-list-entries
-       (cl-loop for id in (gnosis-select 'id 'decks nil t)
-                for output = (gnosis-dashboard-output-deck id)
-                when output
-                collect (list (number-to-string id) (vconcat output))))
-  (tabulated-list-print t)
-  (setf gnosis-dashboard--current `(:type decks :ids ,(gnosis-select 'id 
'decks nil t))))
-
-(defun gnosis-dashboard-decks-add ()
-  "Add deck & refresh."
-  (interactive)
-  (gnosis-add-deck (read-string "Deck name: "))
-  (gnosis-dashboard-output-decks)
-  (revert-buffer t t t))
-
-(defun gnosis-dashboard-decks-suspend-deck (&optional deck-id)
-  "Suspend themata for DECK-ID.
-
-When called with called with a prefix, unsuspend all themata of deck."
-  (interactive)
-  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
-    (gnosis-suspend-deck deck-id)
-    (gnosis-dashboard-output-decks)
-    (revert-buffer t t t)))
-
-(defun gnosis-dashboard-decks-delete (&optional deck-id)
-  "Delete DECK-ID."
-  (interactive)
-  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
-    (gnosis-delete-deck deck-id)
-    (gnosis-dashboard-output-decks)
-    (revert-buffer t t t)))
-
-(defun gnosis-dashboard-decks-view-deck (&optional deck-id)
-  "View themata of DECK-ID."
-  (interactive)
-  (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
-    (gnosis-dashboard-output-themata (gnosis-collect-thema-ids :deck 
deck-id))))
-
-(defun gnosis-dashboard-history (&optional history)
-  "Display review HISTORY."
-  (interactive)
-  (let* ((history (or history
-                     (gnosis-select '[date reviewed-total reviewed-new]
-                                    'activity-log)))
-        (buffer (get-buffer-create "*Gnosis History*")))
-    (with-current-buffer buffer
-      (let ((inhibit-read-only t))
-       (erase-buffer))
-      (tabulated-list-mode)
-      (setq tabulated-list-format
-            `[("Date" ,(/ (window-width) 6) t)
-              ("Total Reviews" ,(/ (window-width) 6) 
gnosis-dashboard-sort-total-themata)
-              ("New" ,(/ (window-width) 6) 
gnosis-dashboard-sort-total-themata)])
-      (make-local-variable 'tabulated-list-entries)
-      ;; Sort for date
-      (setq tabulated-list-sort-key (cons "Date" t))
-      (setq tabulated-list-entries
-            (cl-loop for entry in history
-                     collect (list (car entry)
-                                   (vector (propertize
-                                           (format "%04d/%02d/%02d"
-                                                   (nth 0 (car entry))
-                                                   (nth 1 (car entry))
-                                                   (nth 2 (car entry)))
-                                           'face 'org-date)
-                                           (number-to-string (cadr entry))
-                                           (number-to-string (caddr entry))))))
-      (tabulated-list-init-header)
-      (tabulated-list-print t)
-      (setq gnosis-dashboard--current
-           '(:type history)))
-    (pop-to-buffer buffer)))
-
-(defvar-keymap gnosis-dashboard-mode-map
-  :doc "gnosis-dashboard keymap"
-  "q" #'quit-window
-  "h" #'gnosis-dashboard-menu
-  "H" #'gnosis-dashboard-history
-  "r" #'gnosis-review
-  "a" #'gnosis-add-thema
-  "A" #'gnosis-add-deck
-  "s" #'gnosis-dashboard-suffix-query
-  "n" #'(lambda () (interactive) (gnosis-dashboard-output-themata 
(gnosis-collect-thema-ids)))
-  "d" #'gnosis-dashboard-suffix-decks
-  "t" #'(lambda () (interactive) (gnosis-dashboard-output-tags)))
-
-(define-derived-mode gnosis-dashboard-mode tabulated-list-mode "Gnosis 
Dashboard"
-  "Major mode for displaying Gnosis dashboard."
-  :keymap gnosis-dashboard-mode-map
-  :interactive nil
-  (setq-local header-line-format nil)
-  (setq tabulated-list-padding 2
-       tabulated-list-sort-key nil
-       gnosis-dashboard--selected-ids nil)
-  (display-line-numbers-mode 0))
-
-(defun gnosis-dashboard-enable-mode ()
-  "Enable `gnosis-dashboard-mode'."
-  (when (and (string= (buffer-name) gnosis-dashboard-buffer-name)
-            (not (eq major-mode 'gnosis-dashboard-mode)))
-    (gnosis-dashboard-mode)))
-
-(cl-defun gnosis-dashboard--search (&optional dashboard-type (thema-ids nil))
-  "Display gnosis dashboard.
-
-THEMA-IDS: List of thema ids to display on dashboard.  When nil, prompt
-for dashboard type.
-
-DASHBOARD-TYPE: either Themata or Decks to display the respective dashboard."
-  (interactive)
-  (let ((dashboard-type (or dashboard-type
-                           (cadr (read-multiple-choice
-                                  "Display dashboard for:"
-                                  '((?n "themata")
-                                    (?d "decks")
-                                    (?t "tags")
-                                    (?s "search")))))))
-    (if thema-ids (gnosis-dashboard-output-themata thema-ids)
-      (pcase dashboard-type
-       ("themata" (gnosis-dashboard-output-themata (gnosis-collect-thema-ids)))
-       ("decks" (gnosis-dashboard-output-decks))
-       ("tags"  (gnosis-dashboard-output-themata (gnosis-collect-thema-ids 
:tags t)))
-       ("search" (gnosis-dashboard-search-thema))))
-    (tabulated-list-print t)))
-
-(defun gnosis-dashboard-mark-toggle ()
-  "Toggle mark on the current item in the tabulated-list."
-  (interactive)
-  (let ((inhibit-read-only t)
-        (entry (tabulated-list-get-entry))
-       (id (tabulated-list-get-id)))
-    (if entry
-        (let ((beg (line-beginning-position))
-              (end (line-end-position))
-              (overlays (overlays-in (line-beginning-position) 
(line-end-position))))
-          (if (cl-some (lambda (ov) (overlay-get ov 'gnosis-mark)) overlays)
-              (progn
-                (remove-overlays beg end 'gnosis-mark t)
-               (setq gnosis-dashboard--selected-ids
-                     (remove id gnosis-dashboard--selected-ids)))
-            (let ((ov (make-overlay beg end)))
-             (unless (member id gnosis-dashboard--selected-ids)
-               (setf gnosis-dashboard--selected-ids
-                     (cons id gnosis-dashboard--selected-ids)))
-              (overlay-put ov 'face 'highlight)
-              (overlay-put ov 'gnosis-mark t)))
-         (forward-line))
-      (message "No entry at point"))))
-
-(defun gnosis-dashboard-unmark-all ()
-  "Unmark all items in the tabulated-list."
-  (interactive)
-  (let ((inhibit-read-only t))
-    (setq gnosis-dashboard--selected-ids nil)
-    (remove-overlays nil nil 'gnosis-mark t)
-    (message "All items unmarked")))
-
-(defun gnosis-dashboard-mark-all ()
-  "Mark all items in the tabulated-list buffer and collect their IDs."
-  (interactive)
-  (when (derived-mode-p 'tabulated-list-mode)
-    (let ((inhibit-read-only t))
-      ;; Clear existing marks
-      (remove-overlays (point-min) (point-max) 'gnosis-mark t)
-      ;; Apply overlay to the entire buffer at once
-      (let ((ov (make-overlay (point-min) (point-max))))
-        (overlay-put ov 'face 'highlight)
-        (overlay-put ov 'gnosis-mark t))
-      ;; Set selected IDs
-      (setq gnosis-dashboard--selected-ids gnosis-dashboard-thema-ids)
-      (message "Marked %d items" (count-lines (point-min) (point-max))))))
-
-(defun gnosis-dashboard-marked-delete ()
-  "Delete marked thema entries."
-  (interactive)
-  (when (y-or-n-p "Delete selected themata?")
-    (cl-loop for thema in gnosis-dashboard--selected-ids
-            do (gnosis-delete-thema thema t))
-    (gnosis-dashboard-return)))
-
-(defun gnosis-dashboard-marked-suspend ()
-  "Suspend marked thema entries."
-  (interactive)
-  (when (y-or-n-p "Toggle SUSPEND on selected themata?")
-    (gnosis-toggle-suspend-themata gnosis-dashboard--selected-ids nil)
-    (gnosis-dashboard-return)))
-
-(transient-define-suffix gnosis-dashboard-suffix-query (query)
-  "Search for thema content for QUERY."
-  (interactive "sSearch for thema content: ")
-  (gnosis-dashboard-output-themata (gnosis-collect-thema-ids :query query)))
-
-(transient-define-suffix gnosis-dashboard-suffix-decks ()
-  (interactive)
-  (gnosis-dashboard-output-decks))
-
-(transient-define-prefix gnosis-dashboard-menu ()
-  "Transient buffer for gnosis dashboard interactions."
-  [["Actions"
-    ("r" "Review" gnosis-review)
-    ("a" "Add thema" gnosis-add-thema)
-    ("A" "Add deck" gnosis-add-deck)
-    ("q" "Quit" quit-window)
-    "\n"]
-   ["Themata"
-    ("s" "Search" gnosis-dashboard-suffix-query)
-    ("n" "Themata" (lambda () (interactive)
-                  (gnosis-dashboard-output-themata
-                   (gnosis-collect-thema-ids))))
-    ("d" "Decks" gnosis-dashboard-suffix-decks)
-    ("t" "Tags" (lambda () (interactive)
-                 (gnosis-dashboard-output-tags)))]
-   ["History"
-    ("H" "View Review History" gnosis-dashboard-history)]])
-
-;;;###autoload
-(defun gnosis-dashboard ()
-  "Launch gnosis dashboard."
-  (interactive)
-  (let* ((buffer (get-buffer-create gnosis-dashboard-buffer-name))
-        (due-log (gnosis-review-get--due-themata))
-        (due-thema-ids (mapcar #'car due-log))
-        (inhibit-read-only t))
-    (with-current-buffer buffer
-      (erase-buffer)
-      (gnosis-dashboard-mode)
-      (insert "\n"
-             (gnosis-center-string
-              (format "%s" (propertize "Gnosis Dashboard" 'face
-                                       'gnosis-face-dashboard-header))))
-      (gnosis-insert-separator)
-      (insert (gnosis-center-string
-              (format "\nReviewed today: %s (New: %s)"
-                      (propertize
-                       (number-to-string (gnosis-get-date-total-themata))
-                       'face
-                       'font-lock-variable-use-face)
-                      (propertize
-                       (number-to-string (gnosis-get-date-new-themata))
-                       'face
-                       'font-lock-keyword-face))))
-      (insert "\n")
-      (insert (gnosis-center-string
-              (format "Due themata: %s (Overdue: %s)"
-                      (propertize
-                       (number-to-string (length due-thema-ids))
-                       'face 'error)
-                      (propertize
-                       (number-to-string
-                        (length (gnosis-review-get-overdue-themata)))
-                       'face 'warning))))
-      (insert "\n\n")
-      (insert (gnosis-center-string
-              (format "Daily Average: %s"
-                      (propertize
-                       (gnosis-dashboard-output-average-rev)
-                       'face 'font-lock-type-face))))
-      (insert "\n")
-      (insert (gnosis-center-string
-              (format "Current streak: %s day(s)"
-                      (propertize
-                       (gnosis-dashboard--streak
-                        (gnosis-select 'date 'activity-log '(> reviewed-total 
0) t))
-                       'face 'success))))
-      (insert "\n\n"))
-    (pop-to-buffer-same-window buffer)
-    (goto-char (point-min))
-    (gnosis-dashboard-enable-mode)))
-
 ;; VC functions ;;
 ;;;;;;;;;;;;;;;;;;
 


Reply via email to