;;; 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