summaryrefslogtreecommitdiff
path: root/org-gnosis-ui.el
diff options
context:
space:
mode:
authorThanos Apollo <[email protected]>2024-12-19 01:45:24 +0200
committerThanos Apollo <[email protected]>2024-12-19 01:45:44 +0200
commit96c16bd578a8acc26d9c412df64f63a741c49c64 (patch)
tree8a81bf5b8456e6a871b100066306493d678a54ee /org-gnosis-ui.el
parent1b839dd927ab7b95525543720da3c794e326b5c7 (diff)
Rename to org-gnosis-ui & remove org-roam specific functions.
Diffstat (limited to 'org-gnosis-ui.el')
-rw-r--r--org-gnosis-ui.el747
1 files changed, 747 insertions, 0 deletions
diff --git a/org-gnosis-ui.el b/org-gnosis-ui.el
new file mode 100644
index 0000000..84c10af
--- /dev/null
+++ b/org-gnosis-ui.el
@@ -0,0 +1,747 @@
+;;; org-gnosis-ui.el --- User Interface for Org-Gnosis -*- coding: utf-8; lexical-binding: t; -*-
+
+;; Copyright © 2021 Kirill Rogovoy, Thomas F. K. Jorna
+;; Copyright © 2024 Thanos Apollo
+
+;; author: Kirill Rogovoy, Thomas Jorna, Thanos Apollo
+;; Maintainer: Thanos Apollo <[email protected]>
+;; URL: https://git.thanosapollo.org/org-roam-ui/
+;; Keywords: files outlines
+;; Version: 0.1
+;; Package-Requires: ((emacs "27.1") (simple-httpd "20191103.1446") (websocket "1.13"))
+
+;; This file is NOT part of GNU Emacs.
+
+;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+;;; Commentary:
+;;
+;; org-gnosis-ui provides a web interface for navigating around notes created
+;; within Org-gnosis.
+;;
+;;; Code:
+
+;;; Installing simple-httpd example:
+;; (use-package simple-httpd
+;; :vc (:url "https://github.com/skeeto/emacs-web-server/"))
+
+;;;; Dependencies
+(require 'json)
+(require 'simple-httpd)
+(require 'org-gnosis)
+(require 'websocket)
+
+(defgroup org-gnosis-ui nil
+ "UI in Org-roam."
+ :group 'org-gnosis-ui
+ :prefix "org-gnosis-ui-"
+ :link '(url-link :tag "Github" "https://github.com/org-roam/org-gnosis-ui"))
+
+(defcustom org-gnosis-ui-directory org-gnosis-dir
+ "Directory of org-gnosis-ui notes."
+ :group 'org-gnosis-ui
+ :type 'directory)
+
+(defcustom org-gnosis-ui-port 35902
+ "Roam UI Port."
+ :group 'org-gnosis-ui
+ :type 'number)
+
+(defcustom org-gnosis-ui-sync-theme nil
+ "If true, sync your current Emacs theme with `org-gnosis-ui'.
+Works best with doom-themes.
+Ignored if a custom theme is provied for variable `org-gnosis-ui-custom-theme'."
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-dailies-directory org-gnosis-journal-dir
+ "Dailies/Journaling directory."
+ :group 'org-gnosis-ui
+ :type 'directory)
+
+(defcustom org-gnosis-ui-custom-theme nil
+ "Custom theme for `org-gnosis-ui'.
+Blocks 'org-gnosis-ui-sync-theme from syncing your current theme,
+instead sync this theme.
+Provide a list of cons with the following values:
+bg, bg-alt, fg, fg-alt, red, orange, yellow, green, cyan, blue, violet, magenta.
+E.g. '((bg . \"#1E2029\")
+\(bg-alt . \"#282a36\")
+\(fg . \"#f8f8f2\")
+\(fg-alt . \"#6272a\")
+\(red . \"#ff5555\")
+\(orange . \"#f1fa8c\")
+\(yellow .\"#ffb86c\")
+\(green . \"#50fa7b\")
+\(cyan . \"#8be9fd\")
+\(blue . \"#ff79c6\")
+\(violet . \"#8be9fd\")
+\(magenta . \"#bd93f9\"))."
+ :group 'org-gnosis-ui
+ :type 'list)
+
+(defcustom org-gnosis-ui-follow t
+ "If true, `org-gnosis-ui' will follow you around in the graph."
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-update-on-save t
+ "If true, `org-gnosis-ui' will send new data when you save an `org-roam' buffer.
+This can lead to some jank."
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-open-on-start t
+ "Whether to open your default browser when `org-gnosis-ui' launces."
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-find-ref-title t
+ "Should `org-gnosis-ui' use `org-roam-bibtex' to find a reference's title?"
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-retitle-ref-nodes t
+ "Should `org-gnosis-ui' use `org-roam-bibtex' try to retitle reference nodes?"
+ :group 'org-gnosis-ui
+ :type 'boolean)
+
+(defcustom org-gnosis-ui-ref-title-template
+ "%^{author-abbrev} (%^{year}) %^{title}"
+ "A template for title creation, used for references without associated nodes.
+
+This uses `orb--pre-expand-template' under the hood and therefore only org-style
+capture `%^{...}' are supported."
+ :group 'org-gnosis-ui
+ :type 'string)
+
+(defcustom org-gnosis-ui-browser-function #'browse-url
+ "When non-nil launch org-gnosis-ui with a different browser function.
+Takes a function name, such as #'browse-url-chromium.
+Defaults to #'browse-url."
+ :group 'org-gnosis-ui
+ :type 'function)
+
+(defcustom org-gnosis-ui-tags-function #'(lambda () (org-gnosis-select '* 'tags '1=1 t))
+ "Function that returns all tags as a list."
+ :group 'org-gnosis-ui
+ :type 'function)
+
+(defcustom org-gnosis-ui-nodes-function #'org-gnosis-ui-get-nodes
+ "Function that returns nodes."
+ :group 'org-gnosis-ui
+ :type 'function)
+
+(defcustom org-gnosis-ui-node-query #'org-gnosis-ui-query
+ "Function that returns as ='(id file title level tag).
+
+Output of this function is then used by `org-gnosis-ui-get-nodes' to be
+formatted properly for json data."
+ :group 'org-gnosis-ui
+ :type 'function)
+
+(defcustom org-gnosis-ui-links-function #'(lambda () (org-gnosis-select '* 'links))
+ "Function that returns links."
+ :group 'org-gnosis-ui
+ :type 'function)
+
+;;Hooks
+
+(defcustom org-gnosis-ui-before-open-node-functions nil
+ "Functions to run before a node is opened through org-gnosis-ui.
+Take ID as string as sole argument."
+ :group 'org-gnosis-ui
+ :type 'hook)
+
+(defcustom org-gnosis-ui-after-open-node-functions nil
+ "Functions to run after a node is opened through org-gnosis-ui.
+Take ID as string as sole argument."
+ :group 'org-gnosis-ui
+ :type 'hook)
+
+(defcustom org-gnosis-ui-latex-macros nil
+ "Alist of LaTeX macros to be passed to org-gnosis-ui.
+Format as, i.e. with double backslashes for a single backslash:
+'((\"\\macro\".\"\\something{#1}\"))"
+ :group 'org-gnosis-ui
+ :type 'alist)
+
+;; Internal vars
+
+(defvar org-gnosis-ui-root-dir
+ (concat (file-name-directory
+ (expand-file-name (or
+ load-file-name
+ buffer-file-name)))
+ ".")
+ "Root directory of the org-gnosis-ui project.")
+
+(defvar org-gnosis-ui-app-build-dir
+ (expand-file-name "./out/" org-gnosis-ui-root-dir)
+ "Directory containing org-gnosis-ui's web build.")
+
+(defvar org-gnosis-ui--ws-current-node nil
+ "Var to keep track of which node you are looking at.")
+
+(defvar org-gnosis-ui-ws-socket nil
+ "The websocket for org-gnosis-ui.")
+
+(defvar org-gnosis-ui--window nil
+ "The window for displaying nodes opened from within ORUI.
+This is mostly to prevent issues with EXWM and the Webkit browser.")
+
+(defvar org-gnosis-ui-ws-server nil
+ "The websocket server for org-gnosis-ui.")
+
+;;;###autoload
+(define-minor-mode
+ org-gnosis-ui-mode
+ "Enable org-gnosis-ui.
+This serves the web-build and API over HTTP."
+ :lighter " org-gnosis-ui"
+ :global t
+ :group 'org-gnosis-ui
+ :init-value nil
+ (cond
+ (org-gnosis-ui-mode
+ ;;; check if the default keywords actually exist on `orb-preformat-keywords'
+ ;;; else add them
+ (setq-local httpd-port org-gnosis-ui-port)
+ (setq httpd-root org-gnosis-ui-app-build-dir)
+ (httpd-start)
+ (setq org-gnosis-ui-ws-server
+ (websocket-server
+ 35903
+ :host 'local
+ :on-open #'org-gnosis-ui--ws-on-open
+ :on-message #'org-gnosis-ui--ws-on-message
+ :on-close #'org-gnosis-ui--ws-on-close))
+ (when org-gnosis-ui-open-on-start (org-gnosis-ui-open)))
+ (t
+ (progn
+ (websocket-server-close org-gnosis-ui-ws-server)
+ (httpd-stop)
+ (remove-hook 'after-save-hook #'org-gnosis-ui--on-save)
+ (org-gnosis-ui-follow-mode -1)))))
+
+(defun org-gnosis-ui--log-data (message data)
+ "Log MESSAGE along with DATA for debugging purposes."
+ (message "%s: %s" message (if (stringp data) data (prin1-to-string data))))
+
+(defun org-gnosis-ui--ws-on-open (ws)
+ "Open the websocket WS to org-gnosis-ui and send initial data."
+ (when (websocket-openp ws)
+ (setq org-gnosis-ui-ws-socket ws)
+ (org-gnosis-ui--log-data "WebSocket opened." nil)
+ (condition-case err
+ (progn
+ (org-gnosis-ui--send-variables ws)
+ (org-gnosis-ui--send-graphdata))
+ (error (message "Error while sending data on open: %S" err)))
+ (when org-gnosis-ui-update-on-save
+ (add-hook 'after-save-hook #'org-gnosis-ui--on-save))
+ (when org-gnosis-ui-follow
+ (org-gnosis-ui-follow-mode 1))
+ (message "Connection established with org-gnosis-ui.")))
+
+(defun org-gnosis-ui--ws-on-message (_ws frame)
+ "Functions to run when the org-gnosis-ui server receives a message.
+Takes _WS and FRAME as arguments."
+ (let* ((msg (json-parse-string
+ (websocket-frame-text frame) :object-type 'alist))
+ (command (alist-get 'command msg))
+ (data (alist-get 'data msg)))
+ (cond ((string= command "open")
+ (org-gnosis-ui--on-msg-open-node data))
+ ((string= command "delete")
+ (org-gnosis-ui--on-msg-delete-node data))
+ ((string= command "create")
+ (org-gnosis-ui--on-msg-create-node data))
+ (t
+ (message
+ "Something went wrong when receiving a message from org-gnosis-ui")))))
+
+(defun org-gnosis-ui--on-msg-open-node (data)
+ "Open a node when receiving DATA from the websocket."
+ (let* ((id (alist-get 'id data))
+ (node (org-roam-node-from-id id))
+ (pos (org-roam-node-point node))
+ (buf (find-file-noselect (org-roam-node-file node))))
+ (run-hook-with-args 'org-gnosis-ui-before-open-node-functions id)
+ (unless (window-live-p org-gnosis-ui--window)
+ (if-let ((windows (window-list))
+ (or-windows (seq-filter
+ (lambda (window)
+ (when (bound-and-true-p org-gnosis-mode)
+ (window-buffer window)))
+ windows))
+ (newest-window (car
+ (seq-sort-by
+ #'window-use-time #'> or-windows))))
+ (setq org-gnosis-ui--window newest-window)
+ (split-window-horizontally)
+ (setq org-gnosis-ui--window (frame-selected-window))))
+ (set-window-buffer org-gnosis-ui--window buf)
+ (select-window org-gnosis-ui--window)
+ (goto-char pos)
+ (run-hook-with-args 'org-gnosis-ui-after-open-node-functions id)))
+
+(defun org-gnosis-ui--on-msg-delete-node (data)
+ "Delete a node when receiving DATA from the websocket.
+
+TODO: Be able to delete individual nodes."
+ (progn
+ (message "Deleted %s" (alist-get 'file data))
+ (delete-file (alist-get 'file data))
+ (org-roam-db-sync)
+ (org-gnosis-ui--send-graphdata)))
+
+(defun org-gnosis-ui--on-msg-create-node (data)
+ "Create a node when receiving DATA from the websocket."
+ (progn
+ (if (and (fboundp #'orb-edit-note) (alist-get 'ROAM_REFS data))
+ (orb-edit-note (alist-get 'id data)))
+ (org-roam-capture-
+ :node (org-roam-node-create :title (alist-get 'title data))
+ :props '(:finalize find-file))))
+
+(defun org-gnosis-ui--ws-on-close (_websocket)
+ "What to do when _WEBSOCKET to org-gnosis-ui is closed."
+ (remove-hook 'after-save-hook #'org-gnosis-ui--on-save)
+ (org-gnosis-ui-follow-mode -1)
+ (message "Connection with org-gnosis-ui closed."))
+
+(defun org-gnosis-ui--get-text (id)
+ "Retrieve the text from org-node ID."
+ (let*
+ ((node (org-roam-populate (org-roam-node-create
+ :id id)))
+ (file (org-roam-node-file node)))
+ (org-roam-with-temp-buffer
+ file
+ (when (> (org-roam-node-level node) 0)
+ ;; Heading nodes have level 1 and greater.
+ (goto-char (org-roam-node-point node))
+ (org-narrow-to-element))
+ (buffer-substring-no-properties (buffer-end -1) (buffer-end 1)))))
+
+(defun org-gnosis-ui--send-text (id ws)
+ "Send the text from org-node ID through the websocket WS."
+ (let ((text (org-gnosis-ui--get-text id)))
+ (websocket-send-text ws
+ (json-encode
+ `((type . "orgText")
+ (data . ,text))))))
+
+(defservlet* node/:id text/plain ()
+ "Servlet for accessing node content."
+ (insert (org-gnosis-ui--get-text (org-link-decode id)))
+ (httpd-send-header t "text/plain" 200 :Access-Control-Allow-Origin "*"))
+
+(defservlet* img/:file text/plain ()
+ "Servlet for accessing images found in org-roam files."
+ (progn
+ (httpd-send-file t (org-link-decode file))
+ (httpd-send-header t "text/plain" 200 :Access-Control-Allow-Origin "*")))
+
+(defun org-gnosis-ui--on-save ()
+ "Send graphdata on saving an org-roam buffer.
+
+TODO: Make this only send the changes to the graph data, not the complete graph."
+ (when (bound-and-true-p org-gnosis-mode)
+ (org-gnosis-ui--send-variables org-gnosis-ui-ws-socket)
+ (org-gnosis-ui--send-graphdata)))
+
+(defun org-gnosis-ui--check-orb-keywords ()
+ "Check if the default keywords are in `orb-preformat-keywords', if not, add them."
+ (when (and org-gnosis-ui-retitle-ref-nodes (boundp 'orb-preformat-keywords))
+ (dolist (keyword '("author-abbrev" "year" "title"))
+ (unless (seq-contains-p orb-preformat-keywords keyword)
+ (setq orb-preformat-keywords
+ (append orb-preformat-keywords (list keyword)))))))
+
+(defun org-gnosis-ui--find-ref-title (ref)
+ "Find the title of the bibtex entry keyed by `REF'.
+
+Requires `org-roam-bibtex' and `bibtex-completion' (a dependency of `orb') to be
+loaded. Returns `ref' if an entry could not be found."
+ (if (and org-gnosis-ui-find-ref-title
+ (fboundp 'bibtex-completion-get-entry)
+ (fboundp 'orb--pre-expand-template)
+ (boundp 'orb-preformat-keywords))
+ (if-let ((entry (bibtex-completion-get-entry ref))
+ (orb-preformat-keywords
+ (append orb-preformat-keywords
+ '("author-abbrev" "year" "title"))))
+ ;; Create a fake capture template list, only the actual capture at 3
+ ;; matters. Interpolate the bibtex entries, and extract the filled
+ ;; template from the return value.
+ (nth 3 (orb--pre-expand-template
+ `("" "" plain ,org-gnosis-ui-ref-title-template) entry))
+ ref)
+ ref))
+
+(defun org-gnosis-ui--replace-nth (el n lst)
+ "Non-destructively replace the `N'th element of `LST' with `EL'."
+ (let ((head (butlast lst (- (length lst) n)))
+ (tail (nthcdr (+ n 1) lst)))
+ (append head (list el) tail)))
+
+(defun org-gnosis-ui--citekey-to-ref (citekey)
+ "Convert a CITEKEY property (most likely with a `cite:' prefix) to just a key.
+
+This method is mostly taken from `org-roam-bibtex'
+see https://github.com/org-roam/org-roam-bibtex/blob/919ec8d837a7a3bd25232bdba17a0208efaefb2a/orb-utils.el#L289
+but is has been adapted to operate on a sting instead of a node. Requires
+`org-ref' to be loaded. Returns the `key' or nil if the format does not match
+the `org-ref-cite-re'"
+ (if-let ((boundp 'org-ref-cite-re)
+ (citekey-list (split-string-and-unquote citekey)))
+ (catch 'found
+ (dolist (c citekey-list)
+ (when (string-match org-ref-cite-re c)
+ (throw 'found (match-string 2 c)))))))
+
+(defun org-gnosis-ui--retitle-node (node)
+ "Replace the title of citation NODE with associated notes.
+
+A new title is created using information from the bibliography and formatted
+according to `org-gnosis-ui-ref-title-template', just like the citation nodes with
+a note are. It requires `org-roam-bibtex' and it's dependencies
+\(`bibtex-completion' and `org-ref'\) to be loaded.
+
+Returns the node with an updated title if the current node is a reference node
+and the key was found in the bibliography, otherwise the node is returned
+unchanged."
+ (if-let* (org-gnosis-ui-retitle-ref-nodes
+ ;; set a fake var because if-let(((boundp 'fake-var))) returns true
+ (orcr (boundp 'org-ref-cite-re))
+ (citekey (cdr (assoc "ROAM_REFS" (nth 5 node))))
+ (ref (org-gnosis-ui--citekey-to-ref citekey))
+ (title (org-gnosis-ui--find-ref-title ref)))
+ (org-gnosis-ui--replace-nth title 2 node)
+ node))
+
+(defun org-gnosis-ui--create-fake-node (ref)
+ "Create a fake node for REF without a source note."
+ (list
+ ref
+ ref
+ (org-gnosis-ui--find-ref-title ref)
+ 0
+ 0
+ 'nil
+ `(("ROAM_REFS" . ,(format "cite:%s" ref))
+ ("FILELESS" . t))
+ 'nil))
+
+(defun org-gnosis-ui-get-tags ()
+ "Fetch tags from the org-roam database."
+ (seq-mapcat #'seq-reverse
+ (org-roam-db-query
+ [:select :distinct tag :from tags])))
+
+(defun org-gnosis-ui-get-nodes (&optional nodes links-db)
+ "Fetch nodes and create fake nodes based on LINKS-DB.
+If NODES is provided, use it directly."
+ (let* ((nodes-db (or nodes (org-gnosis-ui--get-nodes)))
+ (fake-nodes (seq-map #'org-gnosis-ui--create-fake-node
+ (delete-dups
+ (mapcar #'cadr
+ (org-gnosis-ui--filter-citations links-db))))))
+ (append (if org-gnosis-ui-retitle-ref-nodes
+ (seq-map #'org-gnosis-ui--retitle-node nodes-db)
+ nodes-db)
+ fake-nodes)))
+
+(defun org-gnosis-ui--send-graphdata (&optional nodes links tags)
+ "Prepare and send graph data to org-gnosis-ui, optionally using NODES, LINKS, and TAGS.
+If not provided, data is retrieved from the org-roam database."
+ (let* ((links-db (or links (funcall org-gnosis-ui-links-function)))
+ (nodes-db (or nodes (funcall org-gnosis-ui-nodes-function)))
+ (tags-db (or tags (funcall org-gnosis-ui-tags-function)))
+ (nodes-alist '())
+ (links-alist '()))
+
+ ;; Convert nodes to alist
+ (dolist (node nodes-db)
+ (push (org-gnosis-ui-sql-to-alist
+ '(id file title level pos olp properties tags) node)
+ nodes-alist))
+
+ ;; Convert links to alist
+ (dolist (link links-db)
+ (push (org-gnosis-ui-sql-to-alist
+ '(source target type) link)
+ links-alist))
+
+ (let ((response `((nodes . ,nodes-alist)
+ (links . ,links-alist)
+ (tags . ,tags-db))))
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "graphdata") (data . ,response)))))))
+
+
+(defun org-gnosis-ui--filter-citations (links)
+ "Filter out the citations from LINKS."
+ (seq-filter
+ (lambda (link)
+ (string-match-p "cite" (nth 2 link)))
+ links))
+
+;; Query for gnosis
+(defun org-gnosis-ui-query ()
+ "Query to get nodes from org-gnosis db."
+ (emacsql org-gnosis-db
+ [:select [id file title level
+ (funcall group-concat tag
+ (emacsql-escape-raw \, ))]
+ :from nodes
+ :as tags
+ :left-join tags
+ :on (= id id)
+ :group :by id]))
+
+(defun org-gnosis-ui--get-nodes ()
+ "Fetch nodes to display in org-gnosis-ui with properly structured output."
+ (mapcar (lambda (node)
+ (let ((id (nth 0 node))
+ (file (nth 1 node))
+ (title (nth 2 node))
+ (level (nth 3 node))
+ (tags (nth 4 node)))
+ ;; Construct the node structure with correct ordering
+ (list id
+ file
+ title
+ level
+ 1 ;; Assume priority or similar static position value
+ nil ;; This corresponds to the `position` from your desired output
+ ;; Properties
+ (list (cons "CATEGORY" (file-name-base file))
+ (cons "ID" id)
+ (cons "BLOCKED" "")
+ (cons "ALLTAGS" (propertize (concat ":" tags ":") 'face 'org-tag)) ;; Assuming tags need to be formatted this way
+ (cons "FILE" file)
+ (cons "PRIORITY" "B"))
+ tags))) ;; Put tags at the last position as per your requirement
+ (funcall org-gnosis-ui-node-query)))
+
+(defun org-gnosis-ui--update-current-node ()
+ "Send the current node data to the web-socket."
+ (when (and (websocket-openp org-gnosis-ui-ws-socket)
+ (bound-and-true-p org-gnosis-mode)
+ (buffer-file-name (buffer-base-buffer)))
+ (let* ((node (org-gnosis-get-id)))
+ (setq org-gnosis-ui--ws-current-node node)
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "command")
+ (data . ((commandName . "follow")
+ (id . ,node)))))))))
+
+
+(defun org-gnosis-ui--update-theme ()
+ "Send the current theme data to the websocket."
+ (let ((ui-theme (list nil)))
+ (if org-gnosis-ui-sync-theme
+ (if (boundp 'doom-themes--colors)
+ (let*
+ ((colors (butlast doom-themes--colors
+ (- (length doom-themes--colors) 25)))
+ doom-theme)
+ (progn
+ (dolist (color colors)
+ (push
+ (cons (car color) (car (cdr color)))
+ doom-theme)))
+ (setq ui-theme doom-theme))
+ (setq ui-theme (org-gnosis-ui-get-theme)))
+ (when org-gnosis-ui-custom-theme
+ (setq ui-theme org-gnosis-ui-custom-theme)))
+ ui-theme))
+
+
+(defun org-gnosis-ui--send-variables (ws)
+ "Send miscellaneous org-roam variables through the websocket WS."
+ (let ((daily-dir (or org-gnosis-ui-dailies-directory ""))
+ (attach-dir (or (and (boundp 'org-attach-id-dir) org-attach-id-dir)
+ (expand-file-name ".attach/" org-directory)))
+ (use-inheritance (or (and (boundp 'org-attach-use-inheritance)
+ org-attach-use-inheritance)
+ nil))
+ (sub-dirs (or (org-gnosis-ui-find-subdirectories) (list ""))))
+ (org-gnosis-ui--log-data "Sending variables"
+ `(("subDirs" . ,sub-dirs)
+ ("dailyDir" . ,daily-dir)
+ ("attachDir" . ,attach-dir)
+ ("useInheritance" . ,use-inheritance)
+ ("roamDir" . ,(or org-gnosis-ui-directory ""))
+ ("katexMacros" . ,(or org-gnosis-ui-latex-macros '()))))
+ (websocket-send-text ws
+ (json-encode
+ `((type . "variables")
+ (data .
+ (("subDirs" . ,sub-dirs)
+ ("dailyDir" . ,daily-dir)
+ ("attachDir" . ,attach-dir)
+ ("useInheritance" . ,use-inheritance)
+ ("roamDir" . ,(or org-gnosis-ui-directory ""))
+ ("katexMacros" . ,(or org-gnosis-ui-latex-macros '())))))))))
+
+(defun org-gnosis-ui-sql-to-alist (column-names rows)
+ "Convert sql result to alist for json encoding.
+ROWS is the sql result, while COLUMN-names is the columns to use."
+ (let (res)
+ (while rows
+ (cond
+ ((not (string= (car column-names) "tags"))
+ (push (cons (pop column-names) (pop rows)) res))
+ ((string= (car column-names) "properties")
+ nil)
+ (t
+ (push (cons (pop column-names)
+ (seq-remove
+ (lambda (elt) (string= elt ","))
+ rows))
+ res)
+ (setq rows nil))))
+ res))
+
+(defun org-gnosis-ui-get-theme ()
+ "Attempt to bring the current theme into a standardized format."
+ (list `(bg . ,(face-background hl-line-face))
+ `(bg-alt . ,(face-background 'default))
+ `(fg . ,(face-foreground 'default))
+ `(fg-alt . ,(face-foreground font-lock-comment-face))
+ `(red . ,(face-foreground 'error))
+ `(orange . ,(face-foreground 'warning))
+ `(yellow . ,(face-foreground font-lock-builtin-face))
+ `(green . ,(face-foreground 'success))
+ `(cyan . ,(face-foreground font-lock-constant-face))
+ `(blue . ,(face-foreground font-lock-keyword-face))
+ `(violet . ,(face-foreground font-lock-constant-face))
+ `(magenta . ,(face-foreground font-lock-preprocessor-face))))
+
+(defun org-gnosis-ui-find-subdirectories ()
+ "Find all the subdirectories in the org-roam directory.
+TODO: Exclude org-attach dirs."
+ (seq-filter
+ (lambda (file) (and (file-directory-p file) (org-gnosis-ui-allowed-directory-p file)))
+ (directory-files-recursively org-gnosis-ui-directory
+ ".*" t #'org-gnosis-ui-allowed-directory-p)))
+
+(defun org-gnosis-ui-allowed-directory-p (dir)
+ "Check whether a DIR should be listed as a filterable dir.
+Hides . directories."
+ (not (string-match-p "\\(\/\\|\\\\\\)\\..*?" dir)))
+
+;;;; interactive commands
+
+;;;###autoload
+(defun org-gnosis-ui-open ()
+ "Ensure `org-gnosis-ui' is running, then open the `org-gnosis-ui' webpage."
+ (interactive)
+ (unless org-gnosis-ui-mode (org-gnosis-ui-mode))
+ (funcall org-gnosis-ui-browser-function
+ (format "http://localhost:%d" org-gnosis-ui-port)))
+
+;;;###autoload
+(defun org-gnosis-ui-node-zoom (&optional id speed padding)
+ "Move the view of the graph to current node.
+or optionally a node of your choosing.
+Optionally takes three arguments:
+The ID of the node you want to travel to.
+The SPEED in ms it takes to make the transition.
+The PADDING around the nodes in the viewport."
+ (interactive)
+ (if-let ((node (or id (org-gnosis-get-id))))
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "command")
+ (data . ((commandName . "zoom")
+ (id . ,node)
+ (speed . ,speed)
+ (padding . ,padding))))))
+ (message "No node found.")))
+
+
+;;;###autoload
+(defun org-gnosis-ui-node-local (&optional id speed padding)
+ "Open the local graph view of the current node.
+Optionally with ID (string), SPEED (number, ms) and PADDING (number, px)."
+ (interactive)
+ (if-let ((node (or id (org-gnosis-get-id))))
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "command")
+ (data . ((commandName . "local")
+ (id . ,node)
+ (speed . ,speed)
+ (padding . ,padding))))))
+ (message "No node found.")))
+
+
+(defun org-gnosis-ui-change-local-graph (&optional id manipulation)
+ "Add or remove current node to the local graph. If not in local mode, open local-graph for this node."
+ (interactive)
+ (if-let ((node (or id (org-gnosis-get-id))))
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "command")
+ (data . ((commandName . "change-local-graph")
+ (id . ,node)
+ (manipulation . ,(or manipulation "add")))))))
+ (message "No node found.")))
+
+;;;###autoload
+(defun org-gnosis-ui-add-to-local-graph (&optional id)
+ "Add current node to the local graph. If not in local mode, open local-graph for this node."
+ (interactive)
+ (org-gnosis-ui-change-local-graph id "add"))
+
+;;;###autoload
+(defun org-gnosis-ui-remove-from-local-graph (&optional id)
+ "Remove current node from the local graph. If not in local mode, open local-graph for this node."
+ (interactive)
+ (org-gnosis-ui-change-local-graph id "remove"))
+
+;;;###autoload
+(defun org-gnosis-ui-sync-theme ()
+ "Sync your current Emacs theme with org-gnosis-ui."
+ (interactive)
+ (websocket-send-text org-gnosis-ui-ws-socket
+ (json-encode `((type . "theme")
+ (data . ,(org-gnosis-ui--update-theme))))))
+
+;;; Obsolete commands
+(define-obsolete-function-alias #'orui-open #'org-gnosis-ui-open "0.1")
+(define-obsolete-function-alias #'orui-node-local #'org-gnosis-ui-node-local "0.1")
+(define-obsolete-function-alias #'orui-node-zoom #'org-gnosis-ui-node-zoom "0.1")
+(define-obsolete-function-alias #'orui-sync-theme #'org-gnosis-ui-sync-theme "0.1")
+
+;;;###autoload
+(define-minor-mode org-gnosis-ui-follow-mode
+ "Set whether ORUI should follow your every move in Emacs."
+ :lighter " org-gnosis-ui"
+ :global t
+ :group 'org-gnosis-ui
+ :init-value nil
+ (if org-gnosis-ui-follow-mode
+ (progn
+ (add-hook 'post-command-hook #'org-gnosis-ui--update-current-node)
+ (message "org-gnosis-ui will now follow you around."))
+ (remove-hook 'post-command-hook #'org-gnosis-ui--update-current-node)
+ (message "org-gnosis-ui will now leave you alone.")))
+
+(provide 'org-gnosis-ui)
+;;; org-gnosis-ui.el ends here