From 1de60dd9768d2e0bd6307475462e0327b4c01cf7 Mon Sep 17 00:00:00 2001 From: Michael Korat Date: Thu, 29 Jan 2026 21:27:10 +0100 Subject: [PATCH 1/2] feat: save/load web settings to local org-roam dir --- components/Tweaks/Storage/StoragePanel.tsx | 38 ++++++++++++++++++++++ components/Tweaks/index.tsx | 14 ++++++++ org-roam-ui.el | 18 ++++++++++ pages/index.tsx | 1 + util/webSocketFunctions.ts | 4 +++ 5 files changed, 75 insertions(+) create mode 100644 components/Tweaks/Storage/StoragePanel.tsx diff --git a/components/Tweaks/Storage/StoragePanel.tsx b/components/Tweaks/Storage/StoragePanel.tsx new file mode 100644 index 00000000..deb19fb2 --- /dev/null +++ b/components/Tweaks/Storage/StoragePanel.tsx @@ -0,0 +1,38 @@ +import { + Button, + VStack +} from '@chakra-ui/react' +import { saveStorage } from '../../../util/webSocketFunctions' + +export interface StoragePanelProps { + webSocket: any +} + +export const StoragePanel = (props: StoragePanelProps) => { + const { webSocket } = props + return ( + + + + + ) +} \ No newline at end of file diff --git a/components/Tweaks/index.tsx b/components/Tweaks/index.tsx index 039885c4..e23f146e 100644 --- a/components/Tweaks/index.tsx +++ b/components/Tweaks/index.tsx @@ -33,6 +33,7 @@ import { usePersistantState } from '../../util/persistant-state' import { PhysicsPanel } from './Physics/PhysicsPanel' import { BehaviorPanel } from './Behavior/BehaviorPanel' import { VisualsPanel } from './Visual/VisualsPanel' +import { StoragePanel } from './Storage/StoragePanel' export interface TweakProps { physics: typeof initialPhysics @@ -54,6 +55,7 @@ export interface TweakProps { setColoring: any local: typeof initialLocal setLocal: any + webSocket: any } export const Tweaks = (props: TweakProps) => { @@ -77,6 +79,7 @@ export const Tweaks = (props: TweakProps) => { setColoring, local, setLocal, + webSocket, } = props const [showTweaks, setShowTweaks] = usePersistantState('showTweaks', false) @@ -229,6 +232,17 @@ export const Tweaks = (props: TweakProps) => { /> + + + + Storage + + + + + diff --git a/org-roam-ui.el b/org-roam-ui.el index e13349b2..1c4a36bc 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -61,6 +61,10 @@ 35901 "Port to serve the org-roam-ui interface.") +(defvar org-roam-ui-web-settings-file-name + "./.org-roam-ui-web-settings" + "Filename for storing the settings from the browser.") + (defcustom org-roam-ui-sync-theme t "If true, sync your current Emacs theme with `org-roam-ui'. Works best with doom-themes. @@ -223,6 +227,8 @@ Takes _WS and FRAME as arguments." (org-roam-ui--on-msg-delete-node data)) ((string= command "create") (org-roam-ui--on-msg-create-node data)) + ((string= command "saveSettings") + (org-roam-ui--on-msg-save-settings data)) (t (message "Something went wrong when receiving a message from org-roam-ui"))))) @@ -270,6 +276,13 @@ TODO: Be able to delete individual nodes." :node (org-roam-node-create :title (alist-get 'title data)) :props '(:finalize find-file)))) +(defun org-roam-ui--on-msg-save-settings (data) + "Save settings from web ui to org-roam directory." + (with-temp-file + (expand-file-name org-roam-ui-web-settings-file-name org-roam-directory) + (insert data))) + + (defun org-roam-ui--ws-on-close (_websocket) "What to do when _WEBSOCKET to org-roam-ui is closed." (remove-hook 'after-save-hook #'org-roam-ui--on-save) @@ -309,6 +322,11 @@ TODO: Be able to delete individual nodes." (httpd-send-file t (org-link-decode file)) (httpd-send-header t "text/plain" 200 :Access-Control-Allow-Origin "*"))) +(defservlet* settings application/json () + "Servlet for accessing the locally stored settings." + (insert-file-contents (expand-file-name org-roam-ui-web-settings-file-name org-roam-directory)) + (httpd-send-header t "application/json" 200 :Access-Control-Allow-Origin "*")) + (defun org-roam-ui--on-save () "Send graphdata on saving an org-roam buffer. diff --git a/pages/index.tsx b/pages/index.tsx index 8770283f..3f550bca 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -570,6 +570,7 @@ export function GraphPage() { overflow="clip" > Date: Sat, 31 Jan 2026 00:43:44 +0100 Subject: [PATCH 2/2] Some settings can get sent to the browser automatically when the websocket is initialized. --- org-roam-ui.el | 22 ++++++++++++++++++++-- pages/index.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/org-roam-ui.el b/org-roam-ui.el index 1c4a36bc..12580b98 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -65,6 +65,9 @@ "./.org-roam-ui-web-settings" "Filename for storing the settings from the browser.") +(defvar org-roam-ui-autoload-settings t + "Automatically sends the settings to the ui when it is opened for the first time") + (defcustom org-roam-ui-sync-theme t "If true, sync your current Emacs theme with `org-roam-ui'. Works best with doom-themes. @@ -212,7 +215,9 @@ This serves the web-build and API over HTTP." (add-hook 'after-save-hook #'org-roam-ui--on-save)) (message "Connection established with org-roam-ui") (when org-roam-ui-follow - (org-roam-ui-follow-mode 1)))) + (org-roam-ui-follow-mode 1)) + (when org-roam-ui-autoload-settings + (org-roam-ui--send-settings org-roam-ui-ws-socket)))) (defun org-roam-ui--ws-on-message (_ws frame) "Functions to run when the org-roam-ui server receives a message. @@ -322,7 +327,7 @@ TODO: Be able to delete individual nodes." (httpd-send-file t (org-link-decode file)) (httpd-send-header t "text/plain" 200 :Access-Control-Allow-Origin "*"))) -(defservlet* settings application/json () +(defservlet settings application/json () "Servlet for accessing the locally stored settings." (insert-file-contents (expand-file-name org-roam-ui-web-settings-file-name org-roam-directory)) (httpd-send-header t "application/json" 200 :Access-Control-Allow-Origin "*")) @@ -621,6 +626,19 @@ from all other links." ("roamDir" . ,org-roam-directory) ("katexMacros" . ,org-roam-ui-latex-macros)))))))) +(defun org-roam-ui--send-settings (ws) + "Send the web-ui settings to the frontend through the websocket WS." + (let ((settings-file (expand-file-name org-roam-ui-web-settings-file-name org-roam-directory))) + (if (file-exists-p settings-file) + (websocket-send-text org-roam-ui-ws-socket + (json-encode + `((type . "settings") + (data . ,(with-temp-buffer + (insert-file-contents settings-file) + (buffer-string))))))))) + + + (defun org-roam-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." diff --git a/pages/index.tsx b/pages/index.tsx index 3f550bca..4bb47b87 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -476,6 +476,31 @@ export function GraphPage() { default: return console.error('unknown message type', message.type) } + case 'settings': { + try { + const settings = JSON.parse(message.data) + if (settings['3d']) setThreeDim(JSON.parse(settings['3d'])); else setThreeDim(false); + if (settings.tagCols) setTagColors(JSON.parse(settings.tagCols)); else setTagColors({}); + if (settings.physics) setPhysics(JSON.parse(settings.physics)); else setPhysics(initialPhysics); + if (settings.filter) setFilter(JSON.parse(settings.filter)); else setFilter(initialFilter); + if (settings.visuals) setVisuals(JSON.parse(settings.visuals)); else setVisuals(initialVisuals); + if (settings.behavior) setBehavior(JSON.parse(settings.behavior)); else setBehavior(initialBehavior); + if (settings.mouse) setMouse(JSON.parse(settings.mouse)); else setMouse(initialMouse); + if (settings.coloring) setColoring(JSON.parse(settings.coloring)); else setColoring(initialColoring); + if (settings.local) setLocal(JSON.parse(settings.local)); else setLocal(initialLocal); + } catch { + console.error("Error reading settings data. Reverting to default values.") + setThreeDim(false); + setTagColors({}); + setPhysics(initialPhysics); + setFilter(initialFilter); + setVisuals(initialVisuals); + setBehavior(initialBehavior); + setMouse(initialMouse); + setColoring(initialColoring); + setLocal(initialLocal); + } + } } }) }, [])