// CavaWidget.qml - audio visualiser via cava raw output import QtQuick import QtQuick.Layouts import Quickshell.Io import ".." Rectangle { id: root property var bars: Array(12).fill(0) property bool silence: bars.every(v => v === 0) readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"] implicitWidth: cavaRow.implicitWidth + Theme.pillPadH * 2 implicitHeight: Theme.barHeight radius: Theme.radius color: cavaHover.containsMouse ? Theme.pillHover : Theme.pill Behavior on color { ColorAnimation { duration: 150 } } RowLayout { id: cavaRow anchors.centerIn: parent spacing: 1 Repeater { model: root.bars.length Text { required property int index text: root.silence ? " " : root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)] font.family: Theme.fontMono font.pixelSize: Theme.fontSize + 1 color: Theme.foreground } } } MouseArea { id: cavaHover anchors.fill: parent hoverEnabled: true onClicked: Exec.run(["pavucontrol"]) } // Write the cava config once at startup, then run cava pointing at it. Component.onCompleted: writeCfg.running = true Process { id: writeCfg running: false // Plain double-quoted string - no JS interpolation, bash sees ${VAR} verbatim. command: ["bash", "-c", "mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" + "[general]\n" + "framerate = 30\n" + "bars = 12\n" + "[input]\n" + "method = pipewire\n" + "source = auto\n" + "[smoothing]\n" + "noise_reduction = 77\n" + "monstercat = 1\n" + "[output]\n" + "method = raw\n" + "raw_target = /dev/stdout\n" + "data_format = ascii\n" + "ascii_max_range = 255\n" + "bar_delimiter = 59\n" + "CFG\n" ] onExited: cavaProc.running = true } Process { id: cavaProc running: false command: ["cava", "-p", "/tmp/qs-cava/cava.ini"] stdout: SplitParser { onRead: (line) => { const parts = line.trim().replace(/;$/, "").split(";"); if (parts.length >= root.bars.length) { root.bars = parts.slice(0, root.bars.length) .map(v => parseInt(v) || 0); } } } onExited: cavaProc.running = true } }