diff options
-rw-r--r-- | org-roam-ui.el | 35 | ||||
-rw-r--r-- | pages/index.tsx | 132 |
2 files changed, 118 insertions, 49 deletions
diff --git a/org-roam-ui.el b/org-roam-ui.el index fe17676..7a84d2f 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -36,6 +36,7 @@ (require 'json) (require 'simple-httpd) (require 'org-roam) +(require 'websocket) (defgroup org-roam-ui nil "UI in Org-roam." @@ -84,6 +85,8 @@ E.g. '((bg . '#1E2029') :group 'org-roam-ui :type 'list) +(defvar org-roam-ui--ws-current-node nil) + ;;;###autoload (define-minor-mode org-roam-ui-mode @@ -98,11 +101,41 @@ This serves the web-build and API over HTTP." (setq httpd-port org-roam-ui-port httpd-root org-roam-ui/app-build-dir) (httpd-start) + (setq org-roam-ui-ws + (websocket-server + 35903 + :host 'local + :on-open (lambda (ws) (progn (setq oru-ws ws) (org-roam-ui--send-graphdata) (message "Connection established with org-roam-ui"))) + :on-close (lambda (_websocket) (setq oru-ws nil) (message "Connection with org-roam-ui closed succesfully.")))) + (add-hook 'post-command-hook #'org-roam-ui--update-current-node) (add-hook 'post-command-hook #'org-roam-ui-update)) (t + (progn (remove-hook 'post-command-hook #'org-roam-ui-update) - (httpd-stop)))) + (remove-hook 'post-command-hook #'org-roam-ui--update-current-node) + (websocket-server-close org-roam-ui-ws) + (delete-process org-roam-ui-ws) + (httpd-stop))))) + +(defun org-roam-ui--send-graphdata () + (let* ((nodes-columns [id file title level]) + (links-columns [source dest type]) + (nodes-db-rows (org-roam-db-query `[:select ,nodes-columns :from nodes])) + (links-db-rows (org-roam-db-query `[:select ,links-columns :from links :where (or (= type "id") (= type "cite"))])) + (response `((nodes . ,(mapcar (apply-partially #'org-roam-ui-sql-to-alist (append nodes-columns nil)) nodes-db-rows)) + (links . ,(mapcar (apply-partially #'org-roam-ui-sql-to-alist '(source target type)) links-db-rows))))) + (websocket-send-text oru-ws (json-encode `((type . "graphdata") (data . ,response)))))) + +(defun org-roam-ui--update-current-node () + (let* ((node (org-roam-id-at-point))) + (unless (string= org-roam-ui--ws-current-node node) + (setq org-roam-ui--ws-current-node node) + (websocket-send-text oru-ws (json-encode `((type . "command") (data . ((commandName . "follow") (id . ,node))))))))) + +(defun org-roam-ui-show-node () + (interactive) + (websocket-send-text oru-ws (json-encode `((type . "command") (data . ((commandName . "follow") (id . ,(org-roam-id-at-point)))))))) (defservlet* graph application/json () (let* ((nodes-columns [id file title level]) diff --git a/pages/index.tsx b/pages/index.tsx index 7ac12cd..d197ae5 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -61,71 +61,107 @@ export function GraphPage() { const fetchGraphData = () => { return fetch('http://localhost:35901/graph') .then((res) => res.json()) - .then((orgRoamGraphData: OrgRoamGraphReponse) => { - const nodesByFile = orgRoamGraphData.nodes.reduce<NodesByFile>((acc, node) => { - return { - ...acc, - [node.file]: [...(acc[node.file] ?? []), node], - } - }, {}) + .then((orgRoamGraphData) => { + parseGraphData(orgRoamGraphData) + }) + } - const fileLinks: OrgRoamLink[] = Object.keys(nodesByFile).flatMap((file) => { - const nodesInFile = nodesByFile[file] ?? [] - // "file node" as opposed to "heading node" - const fileNode = nodesInFile.find((node) => node.level === 0) - const headingNodes = nodesInFile.filter((node) => node.level !== 0) + const parseGraphData = (orgRoamGraphData: OrgRoamGraphReponse) => { + const nodesByFile = orgRoamGraphData.nodes.reduce<NodesByFile>((acc, node) => { + return { + ...acc, + [node.file]: [...(acc[node.file] ?? []), node], + } + }, {}) - if (!fileNode) { - return [] - } + const fileLinks: OrgRoamLink[] = Object.keys(nodesByFile).flatMap((file) => { + const nodesInFile = nodesByFile[file] ?? [] + // "file node" as opposed to "heading node" + const fileNode = nodesInFile.find((node) => node.level === 0) + const headingNodes = nodesInFile.filter((node) => node.level !== 0) - return headingNodes.map((headingNode) => ({ - source: headingNode.id, - target: fileNode.id, - type: 'parent', - })) - }) + if (!fileNode) { + return [] + } - nodeByIdRef.current = Object.fromEntries( - orgRoamGraphData.nodes.map((node) => [node.id, node]), - ) + return headingNodes.map((headingNode) => ({ + source: headingNode.id, + target: fileNode.id, + type: 'parent', + })) + }) - const links = [...orgRoamGraphData.links, ...fileLinks] - linksByNodeIdRef.current = links.reduce<LinksByNodeId>((acc, link) => { - return { - ...acc, - [link.source]: [...(acc[link.source] ?? []), link], - [link.target]: [...(acc[link.target] ?? []), link], - } - }, {}) + nodeByIdRef.current = Object.fromEntries(orgRoamGraphData.nodes.map((node) => [node.id, node])) - const orgRoamGraphDataWithFileLinks = { - ...orgRoamGraphData, - links, - } + const links = [...orgRoamGraphData.links, ...fileLinks] + linksByNodeIdRef.current = links.reduce<LinksByNodeId>((acc, link) => { + return { + ...acc, + [link.source]: [...(acc[link.source] ?? []), link], + [link.target]: [...(acc[link.target] ?? []), link], + } + }, {}) - // react-force-graph modifies the graph data implicitly, - // so we make sure there's no overlap between the objects we pass it and - // nodeByIdRef, linksByNodeIdRef - const orgRoamGraphDataClone = JSON.parse(JSON.stringify(orgRoamGraphDataWithFileLinks)) - setGraphData(orgRoamGraphDataClone) - }) + const orgRoamGraphDataWithFileLinks = { + ...orgRoamGraphData, + links, + } + + // react-force-graph modifies the graph data implicitly, + // so we make sure there's no overlap between the objects we pass it and + // nodeByIdRef, linksByNodeIdRef + const orgRoamGraphDataClone = JSON.parse(JSON.stringify(orgRoamGraphDataWithFileLinks)) + setGraphData(orgRoamGraphDataClone) } useEffect(() => { - const trackEmacs = new EventSource('http://127.0.0.1:35901/current-node-id') - trackEmacs.addEventListener('message', (e) => { - const emacsNodeId = e.data - setEmacsNodeId(emacsNodeId) + //const trackEmacs = new EventSource('http://127.0.0.1:35901/current-node-id') + //trackEmacs.addEventListener('message', (e) => { + // const emacsNodeId = e.data + //setEmacsNodeId(emacsNodeId) + //}) + const socket = new WebSocket('ws://localhost:35903') + socket.addEventListener('open', (e) => { + console.log('Connection with Emacs established') + }) + socket.addEventListener('message', (event) => { + const data = JSON.parse(event.data) + console.log(typeof data.type) + switch (data.type) { + case 'graphdata': + console.log('hey') + parseGraphData(data.data) + break + case 'command': + console.log('command') + switch (data.data.commandName) { + case 'follow': + setEmacsNodeId(data.data.id) + break + case 'zoom': { + const links = linksByNodeIdRef.current[data.data.id!] ?? [] + const nodes = Object.fromEntries( + [ + data.commandData.id! as string, + ...links.flatMap((link) => [link.source, link.target]), + ].map((nodeId) => [nodeId, {}]), + ) + /* zoomToFit(500, 200, (node: OrgRoamNode)=>nodes[node.id!]) */ + console.log(nodes) + } + default: + console.log('oopsie whoopsie') + } + } }) - fetchGraphData() + // fetchGraphData() }, []) useEffect(() => { if (!emacsNodeId) { return } - fetchGraphData() + //fetchGraphData() }, [emacsNodeId]) const [threeDim, setThreeDim] = useState(false) |