repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/audio.js
Raw// audio.js — pure audio mixer logic: clamp/format volume, role coloring, row shaping.
// NO I/O, NO native PipeWire access, NO Quickshell imports. Receives plain field dicts
// (lifted off native nodes by Audio.qml), returns dicts/numbers/strings. Dual-mode:
// node `require` (tests) + QML import. Same pattern as clip.js/launch.js/sysinfo.js.
// --- Nerd Font glyphs (FA4 PUA, verified present in SymbolsNerdFont-Regular). \uXXXX escapes.
var GLYPH_OUT = ""; // speaker / volume-up (output)
var GLYPH_OUT_MUTE = ""; // volume-off / mute (output muted)
var GLYPH_IN = ""; // microphone (input)
var GLYPH_IN_MUTE = ""; // microphone-slash (input muted)
// --- clamp a Number into 0..1.5; null/NaN/non-finite -> 0 ---
function clampVolume(v) {
var n = Number(v);
return isFinite(n) ? Math.max(0, Math.min(1.5, n)) : 0;
}
// --- integer percent of a clamped volume: 1.0->100, 1.5->150, 0->0 ---
function volumePercent(v) {
return Math.round(clampVolume(v) * 100);
}
// --- display name precedence; null-safe -> "Audio". Native props arrive as flat keys. ---
function nodeName(n) {
n = n || {};
return String(n.description
|| n.nickname
|| n.nodeDescription
|| n.applicationName
|| n.mediaName
|| n.name
|| "Audio");
}
// --- normalize a list of channel labels to lowercase strings (enums resolved natively) ---
function channelNames(channels) {
var out = [];
var list = channels || [];
for (var i = 0; i < list.length; i++) {
out.push(String(list[i]).toLowerCase());
}
return out;
}
// --- muted flag lifted off node.audio.muted by the bridge ---
function nodeMuted(n) {
return !!(n && n.muted);
}
// --- role color key by mute/volume (spec §3.2 contract):
// muted -> "warning", volume 0 -> "ui.minor", else "ui.accent" ---
function roleForSink(n) {
if (nodeMuted(n)) return "warning";
if (clampVolume(n && n.volume) <= 0) return "ui.minor";
return "ui.accent";
}
// --- privacy chip label from {mic,cam}; no flags -> "Audio" (Bar chip needs non-empty) ---
function privacyLabel(flags) {
var f = flags || {};
if (f.mic && f.cam) return "Mic Cam";
if (f.mic) return "Mic";
if (f.cam) return "Cam";
return "Audio";
}
// --- glyph for a row by kind + muted state ---
function glyphForRow(kind, muted) {
if (kind === "source") return muted ? GLYPH_IN_MUTE : GLYPH_IN;
return muted ? GLYPH_OUT_MUTE : GLYPH_OUT;
}
// --- shape a raw bridge dict into the view/IPC row. ALL resolution happens here. ---
function shapeRow(n, kind) {
n = n || {};
var vol = clampVolume(n.volume);
return {
id: String(n.id == null ? "" : n.id),
name: nodeName(n),
kind: String(kind || ""),
volume: Math.round(vol * 100) / 100,
percent: volumePercent(vol),
muted: nodeMuted(n),
role: roleForSink(n),
isDefault: !!n.isDefault,
active: !!n.active,
channels: channelNames(n.channels),
glyph: glyphForRow(kind, nodeMuted(n))
};
}
// --- index rows by id ---
function rowsById(rows) {
var out = {};
var list = rows || [];
for (var i = 0; i < list.length; i++) {
out[list[i].id] = list[i];
}
return out;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
clampVolume: clampVolume,
volumePercent: volumePercent,
nodeName: nodeName,
channelNames: channelNames,
nodeMuted: nodeMuted,
roleForSink: roleForSink,
privacyLabel: privacyLabel,
glyphForRow: glyphForRow,
shapeRow: shapeRow,
rowsById: rowsById
};
}