From 96c16bd578a8acc26d9c412df64f63a741c49c64 Mon Sep 17 00:00:00 2001 From: Thanos Apollo Date: Thu, 19 Dec 2024 01:45:24 +0200 Subject: Rename to org-gnosis-ui & remove org-roam specific functions. --- org-gnosis-ui.el | 747 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 org-gnosis-ui.el (limited to 'org-gnosis-ui.el') 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 +;; 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 -- cgit v1.2.3