Luigit
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
    };
}