diff options
-rw-r--r-- | components/config.ts | 6 | ||||
-rw-r--r-- | components/tweaks.tsx | 84 | ||||
-rw-r--r-- | org-roam-ui.el | 102 | ||||
-rw-r--r-- | pages/index.tsx | 119 |
4 files changed, 228 insertions, 83 deletions
diff --git a/components/config.ts b/components/config.ts index c2f8ff5..cc2beb8 100644 --- a/components/config.ts +++ b/components/config.ts @@ -80,8 +80,10 @@ export const initialVisuals = { } export const initialBehavior = { - follow: 'Zoom', - followLocalOrZoom: true, + follow: 'zoom', + localSame: 'add', + zoomPadding: 200, + zoomSpeed: 2000, } export const initialMouse = { diff --git a/components/tweaks.tsx b/components/tweaks.tsx index 35ce3e9..3da32cc 100644 --- a/components/tweaks.tsx +++ b/components/tweaks.tsx @@ -41,7 +41,13 @@ import { } from '@chakra-ui/react' import React, { useState, useContext } from 'react' import Scrollbars from 'react-custom-scrollbars-2' -import { initialPhysics, initialFilter, initialVisuals, initialMouse } from './config' +import { + initialPhysics, + initialFilter, + initialVisuals, + initialMouse, + initialBehavior, +} from './config' import { ThemeContext } from '../pages/themecontext' @@ -56,6 +62,8 @@ export interface TweakProps { setVisuals: any mouse: typeof initialMouse setMouse: any + behavior: typeof initialBehavior + setBehavior: any } export const Tweaks = (props: TweakProps) => { @@ -70,6 +78,8 @@ export const Tweaks = (props: TweakProps) => { setVisuals, mouse, setMouse, + behavior, + setBehavior, } = props const [showTweaks, setShowTweaks] = useState(true) const { highlightColor, setHighlightColor } = useContext(ThemeContext) @@ -1188,6 +1198,78 @@ export const Tweaks = (props: TweakProps) => { </Portal> </Menu> </Flex> + <Flex alignItems="center" justifyContent="space-between"> + <Text>Follow Emacs by...</Text> + <Menu placement="right"> + <MenuButton + as={Button} + rightIcon={<ChevronDownIcon />} + colorScheme="" + color="black" + > + <Text>{behavior.follow[0].toUpperCase() + behavior.follow.slice(1)}</Text> + </MenuButton> + <Portal> + {' '} + <MenuList bgColor="gray.200" zIndex="popover"> + <MenuItem onClick={() => setBehavior({ ...behavior, follow: 'local' })}> + Opening the local graph + </MenuItem> + <MenuItem onClick={() => setBehavior({ ...behavior, follow: 'zoom' })}> + Zooming to the current node + </MenuItem> + </MenuList> + </Portal> + </Menu> + </Flex> + <Flex alignItems="center" justifyContent="space-between"> + <Flex> + <Text>Follow local graph</Text> + <InfoTooltip infoText="When in local mode and opening a node that already exists in Emacs, should I add that local graph or open the new one?" /> + </Flex> + <Menu placement="right"> + <MenuButton + as={Button} + rightIcon={<ChevronDownIcon />} + colorScheme="" + color="black" + > + <Text>{behavior.localSame === 'add' ? 'Add' : 'New'}</Text> + </MenuButton> + <Portal> + {' '} + <MenuList bgColor="gray.200" zIndex="popover"> + <MenuItem + onClick={() => setBehavior({ ...behavior, localSame: 'new' })} + > + Open that nodes graph + </MenuItem> + <MenuItem + onClick={() => setBehavior({ ...behavior, localSame: 'add' })} + > + Add node to local graph + </MenuItem> + </MenuList> + </Portal> + </Menu> + </Flex> + <SliderWithInfo + label="Zoom speed" + value={behavior.zoomSpeed} + min={0} + max={4000} + step={100} + onChange={(value) => setBehavior({ ...behavior, zoomSpeed: value })} + /> + <SliderWithInfo + label="Zoom padding" + value={behavior.zoomPadding} + min={0} + max={400} + step={1} + onChange={(value) => setBehavior({ ...behavior, zoomPadding: value })} + infoText="How much to zoom out to accomodate all nodes when changing the view." + /> </VStack> </AccordionPanel> </AccordionItem> diff --git a/org-roam-ui.el b/org-roam-ui.el index 4ab2513..2b86914 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -119,7 +119,7 @@ This serves the web-build and API over HTTP." :on-close (lambda (_websocket) (remove-hook 'post-command-hook #'org-roam-ui--update-current-node) (remove-hook 'after-save-hook #'org-roam-ui--on-save) - (message "Connection with org-roam-ui closed succesfully.")))) + (message "Connection with org-roam-ui closed.")))) (if (boundp 'counsel-load-theme) (advice-add 'counsel-load-theme :after #'org-roam-ui-sync-theme--advice) @@ -133,9 +133,10 @@ This serves the web-build and API over HTTP." (httpd-stop))))) (defun org-roam-ui--on-save () - "Send graphdata on saving an only org-roam buffer." + "Send graphdata on saving an org-roam buffer." (when (org-roam-buffer-p) - (org-roam-ui--send-graphdata)) + (org-roam-ui--send-graphdata) + (org-roam-ui)) ) (defun org-roam-ui--send-graphdata () @@ -156,20 +157,12 @@ This serves the web-build and API over HTTP." (websocket-send-text oru-ws (json-encode `((type . "command") (data . ((commandName . "follow") (id . ,node)))))))))) -(defun org-roam-ui-show-node () - "Open the current org-roam node in org-roam-ui." - (interactive) - (websocket-send-text oru-ws (json-encode `((type . "command") (data . ((commandName . "follow") (id . ,(org-roam-id-at-point)))))))) -(defun org-roam-ui-sync-theme--advice () - "Function which is called after load-theme to sync your current theme with org-roam-ui." - (message "Syncing theme") - (websocket-send-text oru-ws (json-encode `((type . "theme") (data . ,(org-roam-ui--update-theme)))))) +;; (defun org-roam-ui-sync-theme--advice () +;; "Function which is called after load-theme to sync your current theme with org-roam-ui." +;; (message "Syncing theme") +;; (websocket-send-text oru-ws (json-encode `((type . "theme") (data . ,(org-roam-ui--update-theme)))))) -(defun org-roam-ui-sync-theme-manually () - "Sync your current Emacs theme with org-roam-ui." - (interactive) - (websocket-send-text oru-ws (json-encode `((type . "theme") (data . ,(org-roam-ui--update-theme)))))) (defun org-roam-ui--update-theme () (let ((ui-theme (list nil))) @@ -185,15 +178,15 @@ This serves the web-build and API over HTTP." org-roam-ui-custom-theme)) ui-theme)) -(defservlet* graph application/json () - (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 (json-encode `((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)))))) - (insert response) - (httpd-send-header t "application/json" 200 :Access-Control-Allow-Origin "*"))) +;; (defservlet* graph application/json () +;; (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 (json-encode `((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)))))) +;; (insert response) +;; (httpd-send-header t "application/json" 200 :Access-Control-Allow-Origin "*"))) (defun org-roam-ui-sql-to-alist (column-names rows) "Convert sql result to alist for json encoding. @@ -204,31 +197,31 @@ ROWS is the sql result, while COLUMN-NAMES is the columns to use." res)) -(defservlet* id/:id text/html () - (let ((node (org-roam-populate (org-roam-node-create :id id))) - html-string) - (org-roam-with-temp-buffer (org-roam-node-file node) - (progn - (setq-local org-export-with-toc nil) - (setq-local org-export-with-broken-links t) - (setq-local org-export-with-sub-superscripts nil) - (replace-string "[[id:" "[[./") - (let* ((file-string (buffer-string)) - (matches (s-match-strings-all "\\[\\[\\(file:\\|\\.\\/\\)\\(.*\\.\\(png\\|jpg\\|jpeg\\|gif\\|svg\\)\\)\\]\\(\\[.*\\]\\)?\\]" file-string))) - (dolist (match matches) - (let ((path (elt match 2)) - (link (elt match 0))) - (unless (file-name-absolute-p path) - (setq path (concat (file-name-directory (org-roam-node-file-node)) path))) - (setq path (f-full path)) - (if (file-exists-p path) - (setq file-string - (s-replace link (format "[[image:%s]]" path) file-string))))) - (erase-buffer) - (insert file-string)) - (setq html-string (org-export-as 'html)))) - (insert html-string) - (httpd-send-header t "text/html" 200 :Access-Control-Allow-Origin "*"))) +;; (defservlet* id/:id text/html () +;; (let ((node (org-roam-populate (org-roam-node-create :id id))) +;; html-string) +;; (org-roam-with-temp-buffer (org-roam-node-file node) +;; (progn +;; (setq-local org-export-with-toc nil) +;; (setq-local org-export-with-broken-links t) +;; (setq-local org-export-with-sub-superscripts nil) +;; (replace-string "[[id:" "[[./") +;; (let* ((file-string (buffer-string)) +;; (matches (s-match-strings-all "\\[\\[\\(file:\\|\\.\\/\\)\\(.*\\.\\(png\\|jpg\\|jpeg\\|gif\\|svg\\)\\)\\]\\(\\[.*\\]\\)?\\]" file-string))) +;; (dolist (match matches) +;; (let ((path (elt match 2)) +;; (link (elt match 0))) +;; (unless (file-name-absolute-p path) +;; (setq path (concat (file-name-directory (org-roam-node-file-node)) path))) +;; (setq path (f-full path)) +;; (if (file-exists-p path) +;; (setq file-string +;; (s-replace link (format "[[image:%s]]" path) file-string))))) +;; (erase-buffer) +;; (insert file-string)) +;; (setq html-string (org-export-as 'html)))) +;; (insert html-string) +;; (httpd-send-header t "text/html" 200 :Access-Control-Allow-Origin "*"))) (defun org-roam-ui-get-theme () "Attempt to bring the current theme into a standardized format." @@ -269,22 +262,31 @@ The padding around the nodes in the viewport." ((commandName . "local") (id . ,node))))))) (message "No node found.")) +(defvar org-roam-ui--following nil) (defun orui-toggle-follow () "Set whether ORUI should follow your every move in emacs. Default yes." (interactive) (if (member 'org-roam-ui--update-current-node (default-value 'post-command-hook)) (progn (remove-hook 'post-command-hook #'org-roam-ui--update-current-node) - (message "Org-Roam-UI will now leave you alone.")) + (message "Org-Roam-UI will now leave you alone.") + (setq org-roam-ui--following nil)) (add-hook 'post-command-hook #'org-roam-ui--update-current-node) + (setq org-roam-ui--following nil) (message "Org-Roam-UI will now follow you around.")) ) + (defun orui-toggle-local-zoom () "Toggles whether org-roam-ui should go to the local view of a given node or zoom to it. Defaults to local." (interactive) (org-roam-ui--send-command "toggle" `(id . yes))) +(defun orui-sync-theme () + "Sync your current Emacs theme with org-roam-ui." + (interactive) + (websocket-send-text oru-ws (json-encode `((type . "theme") (data . ,(org-roam-ui--update-theme)))))) + (provide 'org-roam-ui) ;;; org-roam-ui.el ends here diff --git a/pages/index.tsx b/pages/index.tsx index 90d9686..6eb86b1 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -131,15 +131,72 @@ export function GraphPage() { const { setEmacsTheme } = useContext(ThemeContext) const [threeDim, setThreeDim] = useState(false) + const [scope, setScope] = useState<Scope>({ nodeIds: [] }) const graphRef = useRef<any>(null) + const followBehavior = ( + command: string, + emacsNode: string, + speed: number = 2000, + padding: number = 200, + ) => { + const fg = graphRef.current + const links = linksByNodeIdRef.current[emacsNode] ?? [] + const nodes = Object.fromEntries( + [emacsNode as string, ...links.flatMap((link) => [link.source, link.target])].map( + (nodeId) => [nodeId, {}], + ), + ) + if (command === 'zoom') { + if (scope.nodeIds.length) { + setScope({ nodeIds: [] }) + } + setTimeout(() => fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]), 50) + return + } + if (!scope.nodeIds.length) { + setScope({ nodeIds: [emacsNode] }) + setTimeout(() => { + fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]) + }, 50) + return + } + if (behavior.localSame !== 'add') { + setScope({ nodeIds: [emacsNode] }) + setTimeout(() => { + fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]) + }, 50) + return + } + + // if the node is in the scopednodes, add it to scope instead of replacing it + if ( + !scope.nodeIds.includes(emacsNode) || + !scope.nodeIds.some((scopeId: string) => { + return nodes[scopeId] + }) + ) { + setScope({ nodeIds: [emacsNode] }) + setTimeout(() => { + fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]) + }, 50) + return + } + setScope((currentScope: Scope) => ({ + ...currentScope, + nodeIds: [...currentScope.nodeIds, emacsNode as string], + })) + setTimeout(() => fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]), 50) + } + useEffect(() => { const socket = new ReconnectingWebSocket('ws://localhost:35903') socket.addEventListener('open', (event) => { console.log('Connection with Emacs established') }) socket.addEventListener('message', (event) => { + const fg = graphRef.current const message = JSON.parse(event.data) switch (message.type) { case 'graphdata': @@ -148,21 +205,28 @@ export function GraphPage() { return setEmacsTheme(message.data) case 'command': switch (message.data.commandName) { - case 'follow': - return setEmacsNodeId(message.data.id) + case 'local': + const speed = behavior.zoomSpeed + const padding = behavior.zoomPadding + followBehavior('local', message.data.id, speed, padding) + setEmacsNodeId(message.data.id) + break case 'zoom': { - const links = linksByNodeIdRef.current[message.data.id!] ?? [] - const nodes = Object.fromEntries( - [ - message.data.id! as string, - ...links.flatMap((link) => [link.source, link.target]), - ].map((nodeId) => [nodeId, {}]), - ) - const fg = graphRef.current - fg.zoomToFit(2000, 200, (node: OrgRoamNode) => nodes[node.id!]) + const speed = message?.data?.speed || behavior.zoomSpeed + const padding = message?.data?.padding || behavior.zoomPadding + followBehavior('zoom', message.data.id, speed, padding) + setEmacsNodeId(message.data.id) + break } - case 'toggle': { - /* setBehavior({ ...behavior, followLocalorZoom: !behavior.followLocalOrZoom }) */ + case 'follow': { + followBehavior( + behavior.follow, + message.data.id, + behavior.zoomSpeed, + behavior.zoomPadding, + ) + setEmacsNodeId(message.data.id) + break } default: return console.error('unknown message type', message.type) @@ -189,6 +253,8 @@ export function GraphPage() { setVisuals, mouse, setMouse, + behavior, + setBehavior, }} /> <Box position="absolute" alignItems="top"> @@ -205,6 +271,8 @@ export function GraphPage() { visuals, behavior, mouse, + scope, + setScope, }} /> </Box> @@ -223,6 +291,8 @@ export interface GraphProps { visuals: typeof initialVisuals behavior: typeof initialBehavior mouse: typeof initialMouse + scope: Scope + setScope: any } export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { @@ -237,6 +307,8 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { visuals, behavior, mouse, + scope, + setScope, } = props // react-force-graph does not track window size @@ -245,7 +317,6 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const [windowWidth, windowHeight] = useWindowSize() const [hoverNode, setHoverNode] = useState<NodeObject | null>(null) - const [scope, setScope] = useState<Scope>({ nodeIds: [] }) const handleClick = (click: string, node: NodeObject) => { switch (click) { @@ -254,7 +325,7 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { if (scope.nodeIds.includes(node.id as string)) { break } - setScope((currentScope) => ({ + setScope((currentScope: Scope) => ({ ...currentScope, nodeIds: [...currentScope.nodeIds, node.id as string], })) @@ -278,27 +349,15 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { ) } + const centralHighlightedNode = useRef<NodeObject | null>(null) + useEffect(() => { if (!emacsNodeId) { return } - const fg = graphRef.current - if (behavior.followLocalOrZoom) { - setScope({ nodeIds: [emacsNodeId] }) - setTimeout(() => { - fg?.zoomToFit( - 2000, - numberWithinRange(20, 200, windowWidth / 8), - (node: NodeObject) => getNeighborNodes(emacsNodeId)[node.id!], - ) - }, 1) - } else { - fg?.zoomToFit(1000, 200, (node: NodeObject) => getNeighborNodes(emacsNodeId)[node.id!]) - setHoverNode(nodeById[emacsNodeId] as NodeObject) - } + centralHighlightedNode.current = nodeById[emacsNodeId] as NodeObject }, [emacsNodeId]) - const centralHighlightedNode = useRef<NodeObject | null>(null) centralHighlightedNode.current = hoverNode const highlightedNodes = useMemo(() => { if (!centralHighlightedNode.current) { |