feat: custom node display names (aliases)

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<string,string> 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 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-30 20:08:16 +02:00
parent a40e7b24e5
commit cb87cd34ba
3 changed files with 58 additions and 4 deletions

View File

@@ -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<number | null>(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 @@
<rect x={nd.x} y={nd.y} width={nd.width} height="22" rx="4" fill={headerBg} />
<rect x={nd.x} y={nd.y + 16} width={nd.width} height="6" fill={headerBg} />
<text x={nd.x + 6} y={nd.y + 15} font-size="10" font-family="monospace" fill="#ddd" font-weight="bold">
{nd.nick || nd.name}
{displayName(nd)}
</text>
<text x={nd.x + nd.width - 6} y={nd.y + 15} font-size="9" font-family="monospace" fill="#777" text-anchor="end">
[{nd.node_type}]
@@ -680,8 +688,13 @@
{#if nodeContextMenu}
<div class="ctx" style="left:{nodeContextMenu.x}px;top:{nodeContextMenu.y}px" role="menu">
<div class="ctx-title">{nodeContextMenu.nodeName}</div>
<div class="ctx-title">{$patchbay.aliases?.[nodeContextMenu.nodeName] || nodeContextMenu.nodeName}</div>
<button onclick={() => { showPropsDialog = nodeContextMenu!.nodeId; nodeContextMenu = null; }}>Properties</button>
<button onclick={() => {
renameDialog = { pwName: nodeContextMenu!.nodeName };
renameInput = $patchbay.aliases?.[nodeContextMenu!.nodeName] ?? '';
nodeContextMenu = null;
}}>Rename</button>
<button onclick={() => {
fetch('/api/destroy-node', {
method: 'POST',
@@ -699,7 +712,7 @@
{#if nd}
<div class="dialog">
<div class="dialog-header">
<span>Properties: {nd.nick || nd.name}</span>
<span>Properties: {displayName(nd)}</span>
<button class="close" onclick={() => { showPropsDialog = null; }}>X</button>
</div>
<div class="dialog-body">
@@ -730,6 +743,31 @@
{/if}
{/if}
<!-- Rename Node Dialog -->
{#if renameDialog}
<div class="dialog" style="right:auto;left:50%;top:50%;transform:translate(-50%,-50%);width:280px">
<div class="dialog-header">
<span>Rename Node</span>
<button class="close" onclick={() => { renameDialog = null; }}>X</button>
</div>
<div class="dialog-body">
<p class="hint" style="word-break:break-all">{renameDialog.pwName}</p>
<div class="input-row">
<input
class="dlg-input"
bind:value={renameInput}
placeholder="Custom display name (leave blank to reset)"
onkeydown={(e) => { if (e.key === 'Enter') { setAlias(renameDialog!.pwName, renameInput); renameDialog = null; } }}
/>
</div>
<div class="input-row" style="justify-content:flex-end;gap:6px">
<button onclick={() => { setAlias(renameDialog!.pwName, ''); renameDialog = null; }}>Reset</button>
<button onclick={() => { setAlias(renameDialog!.pwName, renameInput); renameDialog = null; }}>Save</button>
</div>
</div>
</div>
{/if}
<!-- Hide Nodes Dialog -->
{#if showHideDialog}
<div class="dialog">

View File

@@ -19,6 +19,7 @@ export const patchbay = writable<PatchbayState>({
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 {

View File

@@ -85,4 +85,5 @@ export interface PatchbayState {
pinned_connections: number[];
hide_rules: string[];
merge_rules: string[];
aliases: Record<string, string>; // PW node name → custom display name
}