branch: externals/gnosis
commit 661d7dc9c409efbdeefe532d43af91bfe67f5bdb
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>
[Refactor] dashboard: Improve transient and add org-gnosis nodes support.
---
gnosis-dashboard.el | 632 +++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 596 insertions(+), 36 deletions(-)
diff --git a/gnosis-dashboard.el b/gnosis-dashboard.el
index c52e73e3af..2e63a5ca1b 100644
--- a/gnosis-dashboard.el
+++ b/gnosis-dashboard.el
@@ -29,6 +29,7 @@
(require 'gnosis-monkeytype)
(require 'gnosis)
+(require 'org-gnosis)
(defface gnosis-face-dashboard-header
'((t :inherit (bold font-lock-constant-face)))
@@ -38,6 +39,22 @@ Avoid using an increased height value as this messes up with
`gnosis-center-string' implementation"
:group 'gnosis)
+(defcustom gnosis-dashboard-nodes-default-sort-column "Backlinks"
+ "Default column to sort nodes dashboard by."
+ :type '(radio (const :tag "Title" "Title")
+ (const :tag "Links (forward links count)" "Links")
+ (const :tag "Backlinks (backlinks count)" "Backlinks")
+ (const :tag "Themata (themata links count)" "Themata"))
+ :group 'gnosis)
+
+(defcustom gnosis-dashboard-nodes-default-sort-ascending nil
+ "Whether to sort nodes dashboard in ascending order.
+
+When nil, sort in descending order (larger values first).
+When non-nil, sort in ascending order (smaller values first)."
+ :type 'boolean
+ :group 'gnosis)
+
(defvar gnosis-dashboard-thema-ids nil
"Store thema ids for dashboard.")
@@ -56,6 +73,18 @@ Avoid using an increased height value as this messes up with
gnosis-dashboard-module-today-stats
gnosis-dashboard-module-average-rev))
+(defvar gnosis-dashboard-themata-history nil
+ "Stack of previous themata views for navigation history.")
+
+(defvar gnosis-dashboard-themata-current-ids nil
+ "Current list of thema IDs being displayed.")
+
+(defvar gnosis-dashboard-nodes-history nil
+ "Stack of previous node views for navigation history.")
+
+(defvar gnosis-dashboard-nodes-current-ids nil
+ "Current list of node IDs being displayed.")
+
(defvar gnosis-dashboard-module-header
(lambda ()
(insert "\n"
@@ -174,15 +203,86 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
(defun gnosis-dashboard-search-thema (&optional str)
"Search for themata with STR."
(interactive)
+ ;; Save current themata view and position to history before showing new
search results
+ (when gnosis-dashboard-themata-current-ids
+ (push (cons (tabulated-list-get-id) gnosis-dashboard-themata-current-ids)
+ gnosis-dashboard-themata-history))
(gnosis-dashboard-output-themata
(gnosis-collect-thema-ids :query (or str (read-string "Search for thema:
")))))
+(defun gnosis-dashboard-filter-themata (&optional str ids)
+ "Filter themata IDS by searching within them for STR.
+If IDS is not provided, use current themata being displayed."
+ (interactive)
+ (let* ((ids (or ids gnosis-dashboard-themata-current-ids))
+ (query (or str (read-string "Filter current themata: "))))
+ ;; Validate inputs
+ (unless ids (user-error "No themata to filter"))
+ (when (string-empty-p query) (user-error "Search query cannot be empty"))
+ ;; Filter and display
+ (let ((filtered (cl-intersection ids
+ (gnosis-collect-thema-ids :query query)
+ :test #'equal)))
+ (if filtered
+ (progn
+ ;; Save current position and IDs to history
+ (push (cons (tabulated-list-get-id) ids)
gnosis-dashboard-themata-history)
+ (gnosis-dashboard-output-themata filtered))
+ (message "No themata match the filter")))))
+
+(defun gnosis-dashboard-themata-back ()
+ "Go back to the previous themata view, nodes view, or main dashboard."
+ (interactive)
+ (cond
+ ;; If themata history exists, go back in themata
+ (gnosis-dashboard-themata-history
+ (let* ((previous (pop gnosis-dashboard-themata-history))
+ (previous-id (car previous))
+ (previous-ids (cdr previous)))
+ (gnosis-dashboard-output-themata previous-ids)
+ ;; Restore cursor position
+ (when previous-id
+ (goto-char (point-min))
+ (while (and (not (eobp))
+ (not (equal (tabulated-list-get-id) previous-id)))
+ (forward-line 1)))))
+ ;; If no themata history but we're in themata mode and nodes history exists
+ ((and (not gnosis-dashboard-themata-history)
+ gnosis-dashboard-themata-mode
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-nodes-back))
+ ;; Otherwise go to main dashboard
+ (t
+ (gnosis-dashboard))))
+
+(transient-define-prefix gnosis-dashboard-themata-mode-menu ()
+ "Transient menu for themata dashboard mode."
+ [["Navigate"
+ ("q" "Back" gnosis-dashboard-themata-back)
+ ("SPC" "Search" gnosis-dashboard-search-thema)
+ ("l" "Filter current" gnosis-dashboard-filter-themata)
+ ("g" "Refresh" gnosis-dashboard-return :transient t)
+ ("RET" "Edit at point" gnosis-dashboard-edit-thema)]
+ ["Edit"
+ ("e" "Edit thema" gnosis-dashboard-edit-thema :transient t)
+ ("a" "Add thema" gnosis-add-thema :transient t)
+ ("s" "Suspend" gnosis-dashboard-suspend-thema :transient t)
+ ("d" "Delete" gnosis-dashboard-delete :transient t)]
+ ["Mark"
+ ("m" "Toggle mark" gnosis-dashboard-mark-toggle :transient t)
+ ("M" "Mark all" gnosis-dashboard-mark-all :transient t)
+ ("u" "Unmark" gnosis-dashboard-mark-toggle :transient t)
+ ("U" "Unmark all" gnosis-dashboard-unmark-all :transient t)]])
+
(defvar-keymap gnosis-dashboard-themata-mode-map
:doc "Keymap for themata dashboard."
- "q" #'gnosis-dashboard
+ "?" #'gnosis-dashboard-themata-mode-menu
+ "h" #'gnosis-dashboard-themata-mode-menu
+ "q" #'gnosis-dashboard-themata-back
"e" #'gnosis-dashboard-edit-thema
"s" #'gnosis-dashboard-suspend-thema
"SPC" #'gnosis-dashboard-search-thema
+ "l" #'gnosis-dashboard-filter-themata
"a" #'gnosis-add-thema
"r" #'gnosis-dashboard-return
"g" #'gnosis-dashboard-return
@@ -194,9 +294,7 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
(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))
+ :keymap gnosis-dashboard-themata-mode-map)
(defun gnosis-dashboard--output-themata (thema-ids)
"Output tabulated-list format for THEMA-IDS."
@@ -229,7 +327,14 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
(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)
+ ;; Disable other dashboard modes
+ (gnosis-dashboard-nodes-mode -1)
+ (gnosis-dashboard-decks-mode -1)
+ (gnosis-dashboard-tags-mode -1)
+ ;; Enable themata mode
+ (gnosis-dashboard-themata-mode 1)
+ ;; Store current thema IDs for history
+ (setq gnosis-dashboard-themata-current-ids thema-ids)
(setf tabulated-list-format `[("Keimenon" ,(/ (window-width) 4) t)
("Hypothesis" ,(/ (window-width) 6) t)
("Answer" ,(/ (window-width) 6) t)
@@ -237,7 +342,8 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
("Type" ,(/ (window-width) 10) t)
("Suspend" ,(/ (window-width) 6) t)]
gnosis-dashboard-thema-ids thema-ids
- tabulated-list-entries nil)
+ tabulated-list-entries nil
+ tabulated-list-sort-key nil) ; Clear sort key when switching views
(make-local-variable 'tabulated-list-entries)
(tabulated-list-init-header)
(let ((inhibit-read-only t)
@@ -320,9 +426,25 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
"View themata for TAG."
(interactive)
(let ((tag (or tag (tabulated-list-get-id))))
+ ;; Clear history for fresh start from tags
+ (setq gnosis-dashboard-themata-history nil)
(gnosis-dashboard-output-themata (gnosis-get-tag-themata tag))))
+(transient-define-prefix gnosis-dashboard-tags-mode-menu ()
+ "Transient menu for tags dashboard mode."
+ [["Navigate"
+ ("RET" "View themata" gnosis-dashboard-tag-view-themata)
+ ("q" "Back to dashboard" gnosis-dashboard)
+ ("g" "Refresh" gnosis-dashboard-return :transient t)]
+ ["Edit"
+ ("e" "Rename tag" gnosis-dashboard-rename-tag :transient t)
+ ("r" "Rename tag" gnosis-dashboard-rename-tag :transient t)
+ ("s" "Suspend tag" gnosis-dashboard-suspend-tag :transient t)
+ ("d" "Delete tag" gnosis-dashboard-delete-tag :transient t)]])
+
(defvar-keymap gnosis-dashboard-tags-mode-map
+ "?" #'gnosis-dashboard-tags-mode-menu
+ "h" #'gnosis-dashboard-tags-mode-menu
"RET" #'gnosis-dashboard-tag-view-themata
"e" #'gnosis-dashboard-rename-tag
"q" #'gnosis-dashboard
@@ -341,7 +463,12 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
(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)
+ ;; Disable other dashboard modes
+ (gnosis-dashboard-themata-mode -1)
+ (gnosis-dashboard-nodes-mode -1)
+ (gnosis-dashboard-decks-mode -1)
+ ;; Enable tags mode
+ (gnosis-dashboard-tags-mode 1)
(setf gnosis-dashboard--current '(:type 'tags))
(setq tabulated-list-format [("Name" 35 t)
("Total Themata" 10
gnosis-dashboard-sort-total-themata)])
@@ -362,7 +489,21 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
(not (and (vectorp item) (seq-empty-p item))))
combined-data))))
+(transient-define-prefix gnosis-dashboard-decks-mode-menu ()
+ "Transient menu for decks dashboard mode."
+ [["Navigate"
+ ("RET" "View deck" gnosis-dashboard-decks-view-deck)
+ ("q" "Back to dashboard" gnosis-dashboard)]
+ ["Edit"
+ ("e" "Rename deck" gnosis-dashboard-rename-deck :transient t)
+ ("r" "Rename deck" gnosis-dashboard-rename-deck :transient t)
+ ("a" "Add deck" gnosis-dashboard-decks-add :transient t)
+ ("s" "Suspend deck" gnosis-dashboard-decks-suspend-deck :transient t)
+ ("d" "Delete deck" gnosis-dashboard-decks-delete :transient t)]])
+
(defvar-keymap gnosis-dashboard-decks-mode-map
+ "?" #'gnosis-dashboard-decks-mode-menu
+ "h" #'gnosis-dashboard-decks-mode-menu
"e" #'gnosis-dashboard-rename-deck
"r" #'gnosis-dashboard-rename-deck
"q" #'gnosis-dashboard
@@ -379,7 +520,12 @@ DATE: Integer, used with `gnosis-algorithm-date' to get
previous dates."
"Return deck contents for gnosis dashboard."
(pop-to-buffer-same-window gnosis-dashboard-buffer-name)
(gnosis-dashboard-enable-mode)
- (gnosis-dashboard-decks-mode)
+ ;; Disable other dashboard modes
+ (gnosis-dashboard-themata-mode -1)
+ (gnosis-dashboard-nodes-mode -1)
+ (gnosis-dashboard-tags-mode -1)
+ ;; Enable decks mode
+ (gnosis-dashboard-decks-mode 1)
(setq tabulated-list-format [("Name" 15 t)
("Total Themata" 10
gnosis-dashboard-sort-total-themata)])
(tabulated-list-init-header)
@@ -420,6 +566,8 @@ When called with called with a prefix, unsuspend all
themata of deck."
"View themata of DECK-ID."
(interactive)
(let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+ ;; Clear history for fresh start from decks
+ (setq gnosis-dashboard-themata-history nil)
(gnosis-dashboard-output-themata (gnosis-collect-thema-ids :deck
deck-id))))
(defun gnosis-dashboard-history (&optional history)
@@ -459,23 +607,23 @@ When called with called with a prefix, unsuspend all
themata of deck."
(defvar-keymap gnosis-dashboard-mode-map
:doc "gnosis-dashboard keymap"
- "q" #'quit-window
+ ;; Transient submenus
+ "n" #'gnosis-dashboard-menu-nodes
+ "t" #'gnosis-dashboard-menu-themata
+ "a" #'gnosis-dashboard-menu-actions
+ ;; Quick access
"h" #'gnosis-dashboard-menu
- "H" #'gnosis-dashboard-history
+ "SPC" #'gnosis-dashboard-search-thema
"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)
+ "q" #'quit-window)
(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)
+ ;; Dashboard always centers content
+ (setq-local gnosis-center-content t)
(setq tabulated-list-padding 2
tabulated-list-sort-key nil
gnosis-dashboard--selected-ids nil)
@@ -577,28 +725,52 @@ DASHBOARD-TYPE: either Themata or Decks to display the
respective dashboard."
(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-nodes ()
+ "Transient menu for node operations."
+ [["Nodes"
+ ("n" "View all nodes" (lambda () (interactive)
+ (setq gnosis-dashboard-nodes-history nil)
+ (gnosis-dashboard-output-nodes)))
+ ("t" "View nodes by tag" gnosis-dashboard-nodes-search-by-tag)
+ ("i" "View isolated nodes" (lambda () (interactive)
+ (setq gnosis-dashboard-nodes-history nil)
+ (gnosis-dashboard-output-nodes)
+ (gnosis-dashboard-nodes-show-isolated)))
+ ("q" "Back" transient-quit-one)]])
+
+(transient-define-prefix gnosis-dashboard-menu-themata ()
+ "Transient menu for themata operations."
+ [["Themata"
+ ("t" "View all themata" (lambda () (interactive)
+ (setq gnosis-dashboard-themata-history nil)
+ (gnosis-dashboard-output-themata
(gnosis-collect-thema-ids))))
+ ("s" "Search themata" gnosis-dashboard-suffix-query)
+ ("d" "View by decks" (lambda () (interactive)
+ (gnosis-dashboard-output-decks)))
+ ("T" "View by tags" (lambda () (interactive)
+ (gnosis-dashboard-output-tags)))
+ ("q" "Back" transient-quit-one)]])
+
+(transient-define-prefix gnosis-dashboard-menu-actions ()
+ "Transient menu for actions."
+ [["Actions"
+ ("r" "Review" gnosis-review)
+ ("t" "Add thema" gnosis-add-thema)
+ ("d" "Add deck" gnosis-add-deck)
+ ("m" "Monkeytype" gnosis-monkeytype-start)
+ ("h" "View history" gnosis-dashboard-history)
+ ("q" "Back" transient-quit-one)]])
(transient-define-prefix gnosis-dashboard-menu ()
"Transient buffer for gnosis dashboard interactions."
- [["Actions"
+ [["Navigate"
+ ("n" "Nodes" gnosis-dashboard-menu-nodes)
+ ("t" "Themata" gnosis-dashboard-menu-themata)
+ ("a" "Actions" gnosis-dashboard-menu-actions)]
+ ["Quick Access"
("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)]])
+ ("h" "History" gnosis-dashboard-history)
+ ("q" "Quit" quit-window)]])
;;;###autoload
(defun gnosis-dashboard ()
@@ -617,7 +789,395 @@ DASHBOARD-TYPE: either Themata or Decks to display the
respective dashboard."
(funcall (symbol-value module)))))
(pop-to-buffer-same-window buffer)
(goto-char (point-min))
- (gnosis-dashboard-enable-mode))))
+ (gnosis-dashboard-enable-mode)
+ (gnosis-dashboard-menu))))
+
+(defun gnosis-dashboard-sort-count (entry1 entry2)
+ "Sort function for numeric count columns.
+Compares ENTRY1 and ENTRY2 by converting string values to numbers."
+ (let* ((col-name (car tabulated-list-sort-key))
+ (col-index (tabulated-list--column-number col-name)))
+ (< (string-to-number (aref (cadr entry1) col-index))
+ (string-to-number (aref (cadr entry2) col-index)))))
+
+(defun gnosis-dashboard-get-themata-links (node-id)
+ "Return list of thema IDs that link to NODE-ID.
+Queries the gnosis database links table where dest = NODE-ID."
+ (gnosis-select 'source 'links `(= dest ,node-id) t))
+
+(defun gnosis-dashboard-get-themata-link-titles (node-id)
+ "Return list of keimenon for themata that link to NODE-ID."
+ (let ((thema-ids (gnosis-dashboard-get-themata-links node-id)))
+ (mapcar (lambda (id)
+ (car (gnosis-select 'keimenon 'themata `(= id ,id) t)))
+ thema-ids)))
+
+(defun gnosis-dashboard-get-backlink-titles (node-id)
+ "Return list of titles for nodes that link to NODE-ID (backlinks)."
+ (let ((backlink-ids (org-gnosis-select 'source 'links `(= dest ,node-id) t)))
+ (mapcar (lambda (id)
+ (car (org-gnosis-select 'title 'nodes `(= id ,id) t)))
+ backlink-ids)))
+
+(defun gnosis-dashboard-get-backlink-ids (node-id)
+ "Return list of node IDs that link to NODE-ID (backlinks)."
+ (org-gnosis-select 'source 'links `(= dest ,node-id) t))
+
+(defun gnosis-dashboard-get-forward-link-ids (node-id)
+ "Return list of node IDs that NODE-ID links to (forward links)."
+ (org-gnosis-select 'dest 'links `(= source ,node-id) t))
+
+(defun gnosis-dashboard-nodes--data (&optional node-ids)
+ "Get nodes data formatted for tabulated-list-mode.
+If NODE-IDS is provided, only get data for those nodes.
+Returns list of (ID [TITLE LINK-COUNT BACKLINK-COUNT THEMATA-LINKS-COUNT])."
+ (let ((nodes-data (org-gnosis-get-nodes-data node-ids)))
+ (mapcar
+ (lambda (node)
+ (let* ((id (nth 0 node))
+ (title (nth 1 node))
+ (forward-links (gnosis-dashboard-get-forward-link-ids id))
+ (link-count (number-to-string (length forward-links)))
+ (backlink-count (number-to-string (nth 2 node)))
+ (themata-links (gnosis-dashboard-get-themata-links id))
+ (themata-links-count (number-to-string (length themata-links))))
+ (list id (vector title link-count backlink-count
themata-links-count))))
+ nodes-data)))
+
+(defun gnosis-dashboard-nodes--show-related (get-ids-fn no-results-msg
&optional display-fn)
+ "Generic function to show related items for node at point.
+
+GET-IDS-FN is a function that takes a node-id and returns a list of related
IDs.
+NO-RESULTS-MSG is displayed when no related items are found.
+DISPLAY-FN is the function to display results (defaults to
`gnosis-dashboard-output-nodes')."
+ (let* ((node-id (tabulated-list-get-id))
+ (related-ids (funcall get-ids-fn node-id))
+ (display-fn (or display-fn #'gnosis-dashboard-output-nodes)))
+ (if related-ids
+ (progn
+ (push (cons node-id gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (funcall display-fn related-ids))
+ (message "%s" no-results-msg))))
+
+(defun gnosis-dashboard-nodes-show-links ()
+ "Show forward links of the node at point."
+ (interactive)
+ (gnosis-dashboard-nodes--show-related
+ #'gnosis-dashboard-get-forward-link-ids
+ "No forward links found for this node"))
+
+(defun gnosis-dashboard-nodes-show-backlinks ()
+ "Show backlinks of the node at point."
+ (interactive)
+ (gnosis-dashboard-nodes--show-related
+ #'gnosis-dashboard-get-backlink-ids
+ "No backlinks found for this node"))
+
+(defun gnosis-dashboard-nodes-show-themata-links ()
+ "Show themata that link to the node at point."
+ (interactive)
+ (let ((node-id (tabulated-list-get-id)))
+ ;; Clear themata history for fresh start in themata view
+ (setq gnosis-dashboard-themata-history nil)
+ (gnosis-dashboard-nodes--show-related
+ #'gnosis-dashboard-get-themata-links
+ "No themata link to this node"
+ #'gnosis-dashboard-output-themata)))
+
+(defun gnosis-dashboard-nodes-show-isolated ()
+ "Show isolated nodes (nodes with no connections at all).
+Isolated nodes have no backlinks, no forward links, and no themata links."
+ (interactive)
+ (let* ((all-nodes-data (org-gnosis-get-nodes-data))
+ (isolated-ids (cl-loop for node in all-nodes-data
+ for id = (nth 0 node)
+ for backlink-count = (nth 2 node)
+ when (and (= backlink-count 0)
+ (= (length
(gnosis-dashboard-get-forward-link-ids id)) 0)
+ (= (length
(gnosis-dashboard-get-themata-links id)) 0))
+ collect id)))
+ (if isolated-ids
+ (progn
+ ;; Save current view and position to history
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ ;; Show isolated nodes
+ (gnosis-dashboard-output-nodes isolated-ids))
+ (message "No isolated nodes found"))))
+
+(defun gnosis-dashboard-nodes-search-by-title (query)
+ "Search ALL nodes by title for QUERY.
+Searches the database for nodes whose titles contain the search term."
+ (interactive "sSearch all nodes by title: ")
+ (when (string-empty-p query)
+ (user-error "Search query cannot be empty"))
+ (let* ((all-nodes (org-gnosis-select '[id title] 'nodes))
+ (matching-ids (cl-loop for node in all-nodes
+ for id = (nth 0 node)
+ for title = (nth 1 node)
+ when (string-match-p (regexp-quote query) title)
+ collect id)))
+ (if matching-ids
+ (progn
+ ;; Save current view to history
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes found with title matching '%s'" query))))
+
+(defun gnosis-dashboard-nodes-filter-by-title (query)
+ "Filter CURRENT nodes by title for QUERY.
+Only searches within currently displayed nodes."
+ (interactive "sFilter current nodes by title: ")
+ (unless gnosis-dashboard-nodes-current-ids
+ (user-error "No nodes to filter"))
+ (when (string-empty-p query)
+ (user-error "Search query cannot be empty"))
+ (let* ((current-nodes (org-gnosis-select '[id title] 'nodes
+ `(in id ,(vconcat
gnosis-dashboard-nodes-current-ids))))
+ (matching-ids (cl-loop for node in current-nodes
+ for id = (nth 0 node)
+ for title = (nth 1 node)
+ when (string-match-p (regexp-quote query) title)
+ collect id)))
+ (if matching-ids
+ (progn
+ ;; Save current view to history
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes in current view match '%s'" query))))
+
+(defun gnosis-dashboard-nodes-search-by-content (query)
+ "Search ALL nodes by file content in org-gnosis-dir.
+Only searches files starting with numbers at root level (non-recursive).
+Each matching file contributes its node ID once to the results."
+ (interactive "sSearch all nodes by content: ")
+ (when (string-empty-p query)
+ (user-error "Search query cannot be empty"))
+ (let* ((files (directory-files org-gnosis-dir t "^[0-9].*\\.org$"))
+ (matching-ids '()))
+ (dolist (file files)
+ (when (file-regular-p file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (when (search-forward query nil t)
+ ;; File contains the search term, get its node ID (once per file)
+ (goto-char (point-min))
+ (when (re-search-forward "^:ID:[[:space:]]+\\([^[:space:]]+\\)"
nil t)
+ (let ((id (match-string 1)))
+ (unless (member id matching-ids)
+ (push id matching-ids))))))))
+ (if matching-ids
+ (progn
+ ;; Save current view to history
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes found matching '%s'" query))))
+
+(defun gnosis-dashboard-nodes-filter-by-content (query)
+ "Filter CURRENT nodes by searching file content.
+Only searches within currently displayed nodes."
+ (interactive "sFilter current nodes by content: ")
+ (unless gnosis-dashboard-nodes-current-ids
+ (user-error "No nodes to filter"))
+ (when (string-empty-p query)
+ (user-error "Search query cannot be empty"))
+ (let* ((files (directory-files org-gnosis-dir t "^[0-9].*\\.org$"))
+ (matching-ids '()))
+ (dolist (file files)
+ (when (file-regular-p file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ ;; Get this file's node ID
+ (when (re-search-forward "^:ID:[[:space:]]+\\([^[:space:]]+\\)" nil
t)
+ (let ((id (match-string 1)))
+ ;; Only check if this node is in current view
+ (when (member id gnosis-dashboard-nodes-current-ids)
+ (goto-char (point-min))
+ (when (search-forward query nil t)
+ (unless (member id matching-ids)
+ (push id matching-ids)))))))))
+ (if matching-ids
+ (progn
+ ;; Save current view to history
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes in current view match '%s'" query))))
+
+(defun gnosis-dashboard-nodes-search-by-tag (tag)
+ "Search ALL nodes by TAG."
+ (interactive
+ (list (completing-read "Search nodes by tag: "
+ (org-gnosis-select 'tag 'tags nil t)
+ nil t)))
+ (when (string-empty-p tag)
+ (user-error "Tag cannot be empty"))
+ (let ((matching-ids (org-gnosis--nodes-by-tag tag)))
+ (if matching-ids
+ (progn
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes found with tag '%s'" tag))))
+
+(defun gnosis-dashboard-nodes-filter-by-tag (tag)
+ "Filter CURRENT nodes by TAG."
+ (interactive
+ (list (completing-read "Filter nodes by tag: "
+ (org-gnosis-select 'tag 'tags nil t)
+ nil t)))
+ (unless gnosis-dashboard-nodes-current-ids
+ (user-error "No nodes to filter"))
+ (when (string-empty-p tag)
+ (user-error "Tag cannot be empty"))
+ (let* ((nodes-with-tag (org-gnosis--nodes-by-tag tag))
+ (matching-ids (cl-intersection gnosis-dashboard-nodes-current-ids
nodes-with-tag
+ :test #'equal)))
+ (if matching-ids
+ (progn
+ (push (cons (tabulated-list-get-id)
gnosis-dashboard-nodes-current-ids)
+ gnosis-dashboard-nodes-history)
+ (gnosis-dashboard-output-nodes matching-ids))
+ (message "No nodes in current view have tag '%s'" tag))))
+
+(defun gnosis-dashboard-nodes-back ()
+ "Go back to the previous nodes view, or to main dashboard if at top level."
+ (interactive)
+ (if gnosis-dashboard-nodes-history
+ (let* ((previous (pop gnosis-dashboard-nodes-history))
+ (previous-id (car previous))
+ (previous-ids (cdr previous)))
+ (gnosis-dashboard-output-nodes previous-ids)
+ ;; Restore cursor position
+ (when previous-id
+ (goto-char (point-min))
+ (while (and (not (eobp))
+ (not (equal (tabulated-list-get-id) previous-id)))
+ (forward-line 1))))
+ ;; No history - go back to main dashboard
+ (gnosis-dashboard)))
+
+(defun gnosis-dashboard-nodes-visit ()
+ "Visit the node at point."
+ (interactive)
+ (let* ((node-id (tabulated-list-get-id))
+ (title (car (org-gnosis-select 'title 'nodes `(= id ,node-id) t))))
+ (org-gnosis-find title)))
+
+(defun gnosis-dashboard-nodes-refresh ()
+ "Refresh the current nodes view."
+ (interactive)
+ (gnosis-dashboard-output-nodes gnosis-dashboard-nodes-current-ids))
+
+(defun gnosis-dashboard-nodes--sort-by (column &optional ascending)
+ "Sort nodes dashboard by COLUMN.
+If ASCENDING is non-nil, sort in ascending order, otherwise descending.
+Moves cursor to the beginning of the buffer after sorting."
+ (setq tabulated-list-sort-key (cons column (not ascending)))
+ (tabulated-list-init-header)
+ (tabulated-list-print t)
+ (goto-char (point-min)))
+
+(transient-define-prefix gnosis-dashboard-nodes-sort-menu ()
+ "Sort menu for nodes dashboard."
+ [["Sort By"
+ ("C-t" "Title" (lambda () (interactive) (gnosis-dashboard-nodes--sort-by
"Title" t)))
+ ("l" "Links" (lambda () (interactive) (gnosis-dashboard-nodes--sort-by
"Links")))
+ ("b" "Backlinks" (lambda () (interactive) (gnosis-dashboard-nodes--sort-by
"Backlinks")))
+ ("t" "Themata" (lambda () (interactive) (gnosis-dashboard-nodes--sort-by
"Themata")))
+ ("q" "Cancel" transient-quit-one)]])
+
+(transient-define-prefix gnosis-dashboard-nodes-search-menu ()
+ "Search menu for searching ALL nodes."
+ [["Search All Nodes"
+ ("C-t" "By title" gnosis-dashboard-nodes-search-by-title)
+ ("c" "By content" gnosis-dashboard-nodes-search-by-content)
+ ("t" "By tag" gnosis-dashboard-nodes-search-by-tag)
+ ("q" "Cancel" transient-quit-one)]])
+
+(transient-define-prefix gnosis-dashboard-nodes-filter-menu ()
+ "Filter menu for filtering CURRENT nodes."
+ [["Filter Current Nodes"
+ ("C-t" "By title" gnosis-dashboard-nodes-filter-by-title)
+ ("c" "By content" gnosis-dashboard-nodes-filter-by-content)
+ ("t" "By tag" gnosis-dashboard-nodes-filter-by-tag)
+ ("q" "Cancel" transient-quit-one)]])
+
+(transient-define-prefix gnosis-dashboard-nodes-mode-menu ()
+ "Transient menu for nodes dashboard mode."
+ [["Navigate"
+ ("RET" "Visit node" gnosis-dashboard-nodes-visit)
+ ("q" "Back" gnosis-dashboard-nodes-back)
+ ("g" "Refresh" gnosis-dashboard-nodes-refresh :transient t)]
+ ["Search/Filter/Sort"
+ ("SPC" "Search all..." gnosis-dashboard-nodes-search-menu)
+ ("l" "Filter current..." gnosis-dashboard-nodes-filter-menu)
+ ("s" "Sort..." gnosis-dashboard-nodes-sort-menu)]
+ ["View"
+ ("f" "Show links" gnosis-dashboard-nodes-show-links)
+ ("b" "Show backlinks" gnosis-dashboard-nodes-show-backlinks)
+ ("t" "Show themata links" gnosis-dashboard-nodes-show-themata-links)
+ ("i" "Show isolated" gnosis-dashboard-nodes-show-isolated)]])
+
+(defvar-keymap gnosis-dashboard-nodes-mode-map
+ :doc "Keymap for nodes dashboard."
+ "?" #'gnosis-dashboard-nodes-mode-menu
+ "h" #'gnosis-dashboard-nodes-mode-menu
+ "q" #'gnosis-dashboard-nodes-back
+ "f" #'gnosis-dashboard-nodes-show-links
+ "b" #'gnosis-dashboard-nodes-show-backlinks
+ "t" #'gnosis-dashboard-nodes-show-themata-links
+ "i" #'gnosis-dashboard-nodes-show-isolated
+ "s" #'gnosis-dashboard-nodes-sort-menu
+ "SPC" #'gnosis-dashboard-nodes-search-menu
+ "l" #'gnosis-dashboard-nodes-filter-menu
+ "g" #'gnosis-dashboard-nodes-refresh
+ "RET" #'gnosis-dashboard-nodes-visit)
+
+(define-minor-mode gnosis-dashboard-nodes-mode
+ "Minor mode for gnosis dashboard nodes output."
+ :keymap gnosis-dashboard-nodes-mode-map)
+
+(defun gnosis-dashboard-output-nodes (&optional node-ids)
+ "Display org-gnosis nodes in dashboard.
+If NODE-IDS is provided, display only those nodes. Otherwise display all nodes.
+Shows title, link count, backlink count, and themata links count."
+ (interactive)
+ (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
+ (gnosis-dashboard-enable-mode)
+ ;; Disable other dashboard modes
+ (gnosis-dashboard-themata-mode -1)
+ (gnosis-dashboard-decks-mode -1)
+ (gnosis-dashboard-tags-mode -1)
+ ;; Enable nodes mode
+ (gnosis-dashboard-nodes-mode 1)
+ (setf tabulated-list-format `[("Title" ,(/ (window-width) 2) t)
+ ("Links" ,(/ (window-width) 8)
gnosis-dashboard-sort-count)
+ ("Backlinks" ,(/ (window-width) 8)
gnosis-dashboard-sort-count)
+ ("Themata" ,(/ (window-width) 8)
gnosis-dashboard-sort-count)]
+ tabulated-list-entries nil
+ ;; Set default sort based on user preferences
+ ;; Note: tabulated-list uses FLIP where t=descending, nil=ascending
+ ;; So we invert gnosis-dashboard-nodes-default-sort-ascending
+ tabulated-list-sort-key (cons
gnosis-dashboard-nodes-default-sort-column
+ (not
gnosis-dashboard-nodes-default-sort-ascending)))
+ (make-local-variable 'tabulated-list-entries)
+ (tabulated-list-init-header)
+ (let* ((inhibit-read-only t)
+ (entries (gnosis-dashboard-nodes--data node-ids))
+ ;; Extract actual node IDs being displayed
+ (displayed-ids (mapcar #'car entries)))
+ (erase-buffer)
+ (insert (format "Loading %s nodes..." (length entries)))
+ (setq tabulated-list-entries entries)
+ ;; Store current node IDs (now always populated)
+ (setq gnosis-dashboard-nodes-current-ids displayed-ids)
+ (tabulated-list-print t)))
(provide 'gnosis-dashboard)
;;; gnosis-dashboard.el ends here