From cb87cd34ba31289abc0a8a1e5cb60ccede95de13 Mon Sep 17 00:00:00 2001 From: joren Date: Mon, 30 Mar 2026 20:08:16 +0200 Subject: [PATCH] feat: custom node display names (aliases) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-click any node → Rename to set a custom label. The alias is shown in the node header and Properties dialog instead of the raw PipeWire name. Leave the input blank (or press Reset) to revert to the PW name. Aliases are stored in aliases: Record inside the patchbay state (keyed by PW node name, which is stable for hardware devices) and persisted automatically with the rest of the patchbay config. Old save files without an aliases key are handled gracefully via the ?? {} fallback. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/GraphCanvas.svelte | 44 ++++++++++++++++++++-- frontend/src/lib/stores.ts | 17 ++++++++- frontend/src/lib/types.ts | 1 + 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/GraphCanvas.svelte b/frontend/src/components/GraphCanvas.svelte index 2f9cbcd..a8248a6 100644 --- a/frontend/src/components/GraphCanvas.svelte +++ b/frontend/src/components/GraphCanvas.svelte @@ -12,6 +12,7 @@ setAutoPin, setAutoDisconnect, saveProfile, loadProfile, deleteProfile, setNodeVolume, setNodeMute, + setAlias, createNullSink, createLoopback, loadModule, getQuantum, setQuantum, } from '../lib/stores'; @@ -29,6 +30,8 @@ let contextMenu = $state<{ x: number; y: number; linkId: number; outputPortId: number; inputPortId: number; pinned: boolean } | null>(null); let nodeContextMenu = $state<{ x: number; y: number; nodeId: number; nodeName: string } | null>(null); let showPropsDialog = $state(null); // node ID or null + let renameDialog = $state<{ pwName: string } | null>(null); + let renameInput = $state(''); // Filters let showAudio = $state(true); @@ -128,6 +131,11 @@ return nodeName; } + // Return custom alias, otherwise nick, otherwise PW name + function displayName(nd: { name: string; nick: string }): string { + return $patchbay.aliases?.[nd.name] || nd.nick || nd.name; + } + // Build computed layout let graphNodes = $derived.by(() => { const n = $nodes; @@ -593,7 +601,7 @@ - {nd.nick || nd.name} + {displayName(nd)} [{nd.node_type}] @@ -680,8 +688,13 @@ {#if nodeContextMenu}
@@ -730,6 +743,31 @@ {/if} {/if} + + {#if renameDialog} +
+
+ Rename Node + +
+
+

{renameDialog.pwName}

+
+ { if (e.key === 'Enter') { setAlias(renameDialog!.pwName, renameInput); renameDialog = null; } }} + /> +
+
+ + +
+
+
+ {/if} + {#if showHideDialog}
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 2247f68..0e4304e 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -19,6 +19,7 @@ export const patchbay = writable({ pinned_connections: [], hide_rules: [], merge_rules: [], + aliases: {}, }); // Port/node lookups @@ -82,7 +83,7 @@ export async function initGraph() { if (res.ok) { const data = await res.json(); if (data && data.profiles) { - patchbay.set(data as PatchbayState); + patchbay.set({ ...data, aliases: data.aliases ?? {} } as PatchbayState); } } } catch {} @@ -357,6 +358,20 @@ export function deleteProfile(name: string) { savePatchbayState(); } +// Node aliases (custom display names, keyed by PW node name) +export function setAlias(pwName: string, alias: string) { + patchbay.update(pb => { + const aliases = { ...pb.aliases }; + if (alias.trim()) { + aliases[pwName] = alias.trim(); + } else { + delete aliases[pwName]; // empty string = remove alias + } + return { ...pb, aliases }; + }); + savePatchbayState(); +} + // Volume control export async function setNodeVolume(nodeId: number, volume: number) { try { diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index df832a5..ad7b92a 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -85,4 +85,5 @@ export interface PatchbayState { pinned_connections: number[]; hide_rules: string[]; merge_rules: string[]; + aliases: Record; // PW node name → custom display name }