From d4467e907cba5eeeb793b2168791fd81be124c9a Mon Sep 17 00:00:00 2001 From: blacku Date: Fri, 24 Jul 2026 00:57:39 +0200 Subject: [PATCH] Add Audio Switcher plugin (#82) * feat: add Audio Switcher plugin * fix: hide null audio descriptions * docs: add focused panel screenshot * docs: focus thumbnail on plugin panel --- audio-switcher/README.md | 102 +++ audio-switcher/panel.luau | 501 +++++++++++ audio-switcher/plugin.toml | 44 + audio-switcher/screenshots/panel.webp | Bin 0 -> 20580 bytes audio-switcher/service.luau | 1160 +++++++++++++++++++++++++ audio-switcher/thumbnail.webp | Bin 0 -> 41486 bytes audio-switcher/translations/en.json | 101 +++ audio-switcher/widget.luau | 126 +++ 8 files changed, 2034 insertions(+) create mode 100644 audio-switcher/README.md create mode 100644 audio-switcher/panel.luau create mode 100644 audio-switcher/plugin.toml create mode 100644 audio-switcher/screenshots/panel.webp create mode 100644 audio-switcher/service.luau create mode 100644 audio-switcher/thumbnail.webp create mode 100644 audio-switcher/translations/en.json create mode 100644 audio-switcher/widget.luau diff --git a/audio-switcher/README.md b/audio-switcher/README.md new file mode 100644 index 0000000..e368f33 --- /dev/null +++ b/audio-switcher/README.md @@ -0,0 +1,102 @@ +# Audio Switcher + +Audio Switcher puts PipeWire inputs, outputs, volume controls, and Bluetooth +audio handoff in one compact Noctalia panel. Devices can be renamed or hidden, +and compositor keybinds can cycle devices or connect a specific Bluetooth +device by its persistent number. + +![Audio Switcher panel](screenshots/panel.webp) + +Add the **Audio Switcher** widget from Noctalia's bar editor. Left click opens +the panel, right click cycles through visible outputs, and middle click cycles +through visible inputs. Scrolling over it changes the output volume. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `blackbartblues/audio-switcher` | +| Entries | Widget: `widget`; panel: `audio-switcher`; service: `service` | + +## Requirements + +Install `pactl`, `bluetoothctl`, and `sleep` on `PATH`. On Arch Linux they are +provided by the `libpulse`, `bluez-utils`, and `coreutils` packages respectively. +PipeWire's PulseAudio compatibility service and BlueZ must be running. + +## Usage + +Open the panel from a configured bar widget or run: + +```sh +noctalia msg panel-toggle blackbartblues/audio-switcher:audio-switcher +``` + +The top sliders control the default output and input volume. Select **Outputs** +or **Inputs**, then choose **Use**. For a disconnected Bluetooth output the same +button connects it, waits for its PipeWire endpoint, makes it the default, and +moves current playback streams to it. + +Use the pencil button to set a local display name, choose the device icon, and +change a Bluetooth keybind number. The number is assigned automatically after a +device connects successfully for the first time and can then be changed. Hidden +devices remain available in the panel but are skipped by cycling commands. + +Use the settings button in the panel header to open Noctalia's plugin settings. + +## Settings + +| Entry | Setting | Type | Default | Description | +| --- | --- | --- | --- | --- | +| Plugin | `show_percentage` | `bool` | `true` | Show the output volume beside the bar icon; disable it for an icon-only widget. | +| Plugin | `scroll_step` | `int` | `5` | Volume points changed by each wheel step over the bar widget (1–25). | + +## IPC and keybinds + +The background service exposes commands that can be used by any compositor: + +```sh +# Next non-hidden output. Disconnected Bluetooth outputs are connected as needed. +noctalia msg plugin blackbartblues/audio-switcher:service all cycle-output + +# Next non-hidden, currently available input. +noctalia msg plugin blackbartblues/audio-switcher:service all cycle-input + +# Connect the Bluetooth device assigned to number 2 and use its output. +noctalia msg plugin blackbartblues/audio-switcher:service all connect 2 + +# Refresh device state. +noctalia msg plugin blackbartblues/audio-switcher:service all refresh +``` + +For example, Niri bindings can spawn the commands directly: + +```kdl +Mod+F9 { spawn "noctalia" "msg" "plugin" "blackbartblues/audio-switcher:service" "all" "cycle-output"; } +Mod+F10 { spawn "noctalia" "msg" "plugin" "blackbartblues/audio-switcher:service" "all" "cycle-input"; } +Mod+1 { spawn "noctalia" "msg" "plugin" "blackbartblues/audio-switcher:service" "all" "connect" "1"; } +``` + +Equivalent Hyprland bindings: + +```ini +bind = SUPER, F9, exec, noctalia msg plugin blackbartblues/audio-switcher:service all cycle-output +bind = SUPER, F10, exec, noctalia msg plugin blackbartblues/audio-switcher:service all cycle-input +bind = SUPER, 1, exec, noctalia msg plugin blackbartblues/audio-switcher:service all connect 1 +``` + +## Notes + +Preferences are written to `preferences.json` in Noctalia's data directory for +this plugin. They contain only aliases, hidden flags, remembered Bluetooth input +capabilities, MAC addresses, keybind numbers, and per-device icon choices. + +Before connecting a requested Bluetooth audio device, Audio Switcher disconnects +other Bluetooth audio devices connected to this computer. It cannot disconnect +the target from another computer or phone; that device must release the target +first unless it supports multipoint connections. + +The plugin does not access the network. Its service spawns only the declared +`pactl`, `bluetoothctl`, and `sleep` commands. The short sleep is used while +waiting for a newly connected Bluetooth endpoint to appear. The panel may also +invoke the local Noctalia executable to open the plugin settings page. diff --git a/audio-switcher/panel.luau b/audio-switcher/panel.luau new file mode 100644 index 0000000..0cfdb53 --- /dev/null +++ b/audio-switcher/panel.luau @@ -0,0 +1,501 @@ +--!nonstrict + +local SNAPSHOT_KEY = "audio_switcher_snapshot" +local COMMAND_KEY = "audio_switcher_command" +local RESULT_KEY = "audio_switcher_result" + +local snapshot = noctalia.state.get(SNAPSHOT_KEY) or { + available = false, + bluetoothAvailable = false, + loading = true, + busy = false, + scanning = false, + outputs = {}, + inputs = {}, + defaultOutputId = "", + defaultInputId = "", + outputVolume = 0, + inputVolume = 0, + outputMuted = false, + inputMuted = false, + error = "", + updatedAt = 0, +} + +local activeTab = "output" +local showHidden = false +local editingId = nil +local editAlias = "" +local editIconStyle = "automatic" +local editSlot = "" +local feedback = "" +local feedbackError = false +local requestCounter = 0 +-- Slider onChange is also emitted when the host applies a new controlled value. +-- Keep the last callback value only for onDragEnd; rendering from it would turn +-- the first external update into a sticky local draft and freeze later updates. +local outputVolumeCommitValue = nil +local inputVolumeCommitValue = nil +local volumeControlRevision = 0 +local env = getfenv() +local render + +local ICON_STYLE_VALUES = { "automatic", "speaker", "over_ear", "tws", "wired" } + +local function tr(key, substitutions) + return noctalia.tr(key, substitutions) +end + +local function asArray(value) + return type(value) == "table" and value or {} +end + +local function nextRequestId() + requestCounter += 1 + return "panel-" .. tostring(requestCounter) +end + +local function sendCommand(action, values) + local command = { action = action, requestId = nextRequestId() } + if type(values) == "table" then + for key, value in pairs(values) do command[key] = value end + end + noctalia.state.set(COMMAND_KEY, command) + return command.requestId +end + +local function rowCallback(prefix, index, callback) + local name = prefix .. "_" .. tostring(index) + env[name] = callback + return name +end + +local function activeDevice(kind) + local devices = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs) + for _, device in ipairs(devices) do + if device.active then return device end + end + return nil +end + +local function visibleCount(kind) + local count = 0 + local devices = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs) + for _, device in ipairs(devices) do + if not device.hidden then count += 1 end + end + return count +end + +local function hiddenCount(kind) + local count = 0 + local devices = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs) + for _, device in ipairs(devices) do + if device.hidden then count += 1 end + end + return count +end + +local function selectedIndex(values, selected) + for index, value in ipairs(values) do + if value == selected then return index - 1 end + end + return 0 +end + +local function volumeCard(kind) + local isOutput = kind == "output" + local device = activeDevice(kind) + local volume = tonumber(isOutput and snapshot.outputVolume or snapshot.inputVolume) or 0 + local muted = isOutput and snapshot.outputMuted == true or snapshot.inputMuted == true + local icon = isOutput and (muted and "volume-off" or "volume") or (muted and "microphone-mute" or "microphone") + return ui.column({ flexGrow = 1, gap = 8, padding = 12, radius = 12, fill = "surface_variant/0.45" }, { + ui.row({ align = "center", gap = 8 }, { + ui.glyph({ name = isOutput and "volume" or "microphone", size = 18, color = isOutput and "primary" or "tertiary" }), + ui.column({ flexGrow = 1, gap = 2 }, { + ui.label({ text = tr(isOutput and "panel.output_volume" or "panel.input_volume"), fontWeight = "bold" }), + ui.label({ + text = device and device.name or tr("panel.no_device"), + fontSize = 11, + color = "on_surface_variant", + maxLines = 1, + }), + }), + ui.label({ text = tostring(volume) .. "%", fontSize = 11, color = "on_surface_variant" }), + ui.button({ + glyph = icon, + variant = muted and "destructive" or "ghost", + controlSize = "sm", + tooltip = tr(muted and "panel.unmute" or "panel.mute"), + enabled = device ~= nil, + onClick = isOutput and "onToggleOutputMute" or "onToggleInputMute", + }), + }), + ui.slider({ + key = "volume-" .. kind .. "-" .. tostring(volumeControlRevision), + min = 0, + max = 100, + step = 1, + value = volume, + enabled = device ~= nil, + onChange = isOutput and "onOutputVolumeChange" or "onInputVolumeChange", + onDragEnd = isOutput and "onOutputVolumeCommit" or "onInputVolumeCommit", + }), + }) +end + +local function deviceStatus(device) + if device.active then return tr("device.active") end + if device.bluetooth and not device.available then return tr("device.bluetooth_disconnected") end + if device.bluetooth then return tr("device.bluetooth_connected") end + return tr("device.available") +end + +local function deviceSubtitle(device) + local status = deviceStatus(device) + local description = noctalia.string.trim(tostring(device.description or "")) + if description == "" or description == "(null)" or description:lower() == "null" then return status end + return status .. " · " .. description +end + +local function editor(device) + local children = { + ui.row({ align = "center", gap = 8 }, { + ui.glyph({ name = "edit", size = 16, color = "primary" }), + ui.label({ text = tr("editor.title"), fontWeight = "bold", flexGrow = 1 }), + ui.button({ glyph = "close", variant = "ghost", controlSize = "sm", onClick = "onCancelEdit" }), + }), + ui.label({ text = tr("editor.alias"), fontSize = 11, color = "on_surface_variant" }), + ui.input({ + key = "alias-" .. tostring(device.id), + value = editAlias, + placeholder = device.description, + onChange = "onEditAliasChange", + }), + ui.label({ text = tr("editor.icon"), fontSize = 11, color = "on_surface_variant" }), + ui.select({ + options = { + tr("editor.icon_options.automatic"), + tr("editor.icon_options.speaker"), + tr("editor.icon_options.over_ear"), + tr("editor.icon_options.tws"), + tr("editor.icon_options.wired"), + }, + selectedIndex = selectedIndex(ICON_STYLE_VALUES, editIconStyle), + width = 220, + onChange = "onEditIconChange", + }), + } + if device.bluetooth then + table.insert(children, ui.label({ text = tr("editor.slot"), fontSize = 11, color = "on_surface_variant" })) + table.insert(children, ui.input({ + key = "slot-" .. tostring(device.id), + value = editSlot, + placeholder = "1", + onChange = "onEditSlotChange", + })) + table.insert(children, ui.label({ text = tr("editor.slot_hint"), fontSize = 10, color = "on_surface_variant", maxLines = 2 })) + end + table.insert(children, ui.row({ justify = "end", gap = 8 }, { + ui.button({ text = tr("actions.cancel"), variant = "ghost", onClick = "onCancelEdit" }), + ui.button({ text = tr("actions.save"), glyph = "device-floppy", variant = "primary", onClick = "onSaveEdit" }), + })) + return ui.column({ gap = 7, padding = 10, radius = 9, fill = "surface_variant/0.45", border = "primary/0.35", borderWidth = 1 }, children) +end + +local function deviceRow(device, index, kind) + local useAction = rowCallback("onUse", index, function() + sendCommand(kind == "output" and "set_output" or "set_input", { id = device.id }) + end) + local editAction = rowCallback("onEdit", index, function() + editingId = device.id + editAlias = tostring(device.name or "") + editIconStyle = tostring(device.iconStyle or "automatic") + editSlot = device.slot and tostring(device.slot) or "" + feedback = "" + render() + end) + local hideAction = rowCallback("onHide", index, function() + sendCommand("set_hidden", { id = device.id, kind = kind, hidden = not device.hidden }) + end) + local actionText = device.active and tr("device.current") + or (device.bluetooth and not device.available and tr("actions.connect_and_use") or tr("actions.use")) + local actionVariant = device.active and "ghost" or "primary" + local statusColor = device.active and "secondary" or "on_surface_variant" + local row = ui.column({ gap = 6 }, { + ui.row({ align = "center", gap = 8, padding = 7, radius = 9, border = device.active and "primary" or "outline/0.55", borderWidth = 1, fill = device.active and "primary/0.07" or "surface/0" }, { + ui.glyph({ name = device.icon or (kind == "output" and "volume" or "microphone"), size = 19, color = device.active and "primary" or "on_surface_variant" }), + ui.column({ flexGrow = 1, gap = 2 }, { + ui.label({ text = tostring(device.name or device.description or device.id), fontWeight = "bold", maxLines = 1 }), + ui.label({ + text = deviceSubtitle(device), + fontSize = 10, + color = statusColor, + maxLines = 1, + }), + }), + ui.label({ + text = device.slot and ("#" .. tostring(device.slot)) or "", + color = "primary", + fontSize = 11, + fontWeight = "bold", + visible = device.bluetooth == true, + }), + ui.button({ + text = actionText, + variant = actionVariant, + controlSize = "sm", + enabled = not device.active and snapshot.busy ~= true, + onClick = useAction, + }), + ui.button({ glyph = "edit", variant = "ghost", controlSize = "sm", tooltip = tr("actions.edit"), onClick = editAction }), + ui.button({ + glyph = device.hidden and "eye" or "eye-off", + variant = "ghost", + controlSize = "sm", + tooltip = tr(device.hidden and "actions.show" or "actions.hide"), + onClick = hideAction, + }), + }), + }) + local children = { row } + if editingId == device.id then table.insert(children, editor(device)) end + return ui.column({ gap = 6 }, children) +end + +local function deviceList(kind) + local source = kind == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs) + local rows = {} + local callbackIndex = kind == "output" and 0 or 1000 + for _, device in ipairs(source) do + if showHidden or not device.hidden then + table.insert(rows, deviceRow(device, callbackIndex, kind)) + callbackIndex += 1 + end + end + if #rows == 0 then + table.insert(rows, ui.column({ align = "center", justify = "center", gap = 8, padding = 24 }, { + ui.glyph({ name = kind == "output" and "volume-off" or "microphone-off", size = 32, color = "on_surface_variant" }), + ui.label({ text = tr(showHidden and "panel.no_devices" or "panel.no_visible_devices"), color = "on_surface_variant", textAlign = "center" }), + })) + end + return ui.column({ gap = 6 }, rows) +end + +local function header() + local output = activeDevice("output") + local input = activeDevice("input") + local subtitle = (output and output.name or tr("panel.no_output")) .. " · " .. (input and input.name or tr("panel.no_input")) + return ui.row({ align = "center", gap = 8 }, { + ui.glyph({ name = "switch-horizontal", size = 24, color = "primary" }), + ui.column({ flexGrow = 1, gap = 2 }, { + ui.label({ text = tr("title"), fontSize = 16, fontWeight = "bold" }), + ui.label({ text = subtitle, fontSize = 11, color = "on_surface_variant", maxLines = 1 }), + }), + ui.button({ + glyph = "bluetooth", + variant = snapshot.scanning and "primary" or "ghost", + tooltip = tr(snapshot.scanning and "panel.scanning" or "panel.scan_bluetooth"), + enabled = snapshot.bluetoothAvailable == true and snapshot.scanning ~= true, + onClick = "onScanBluetooth", + }), + ui.button({ glyph = "refresh", variant = "ghost", tooltip = tr("actions.refresh"), onClick = "onRefresh" }), + ui.button({ glyph = "settings", variant = "ghost", tooltip = tr("actions.settings"), onClick = "onOpenSettingsClicked" }), + ui.button({ glyph = "close", variant = "ghost", tooltip = tr("actions.close"), onClick = "onCloseClicked" }), + }) +end + +local function toolbar() + local kind = activeTab + local hidden = hiddenCount(kind) + return ui.row({ align = "center", gap = 4 }, { + ui.button({ + text = tr("tabs.outputs"), + glyph = "volume", + variant = activeTab == "output" and "primary" or "ghost", + selected = activeTab == "output", + onClick = "onShowOutputs", + }), + ui.button({ + text = tr("tabs.inputs"), + glyph = "microphone", + variant = activeTab == "input" and "primary" or "ghost", + selected = activeTab == "input", + onClick = "onShowInputs", + }), + ui.spacer({ flexGrow = 1 }), + ui.label({ + text = tr("panel.visible_count", { count = visibleCount(kind) }), + fontSize = 10, + color = "on_surface_variant", + }), + ui.button({ + text = hidden > 0 and tr("panel.hidden_count", { count = hidden }) or tr("panel.hidden"), + glyph = showHidden and "eye" or "eye-off", + variant = showHidden and "primary" or "ghost", + selected = showHidden, + enabled = hidden > 0 or showHidden, + onClick = "onToggleHidden", + }), + ui.button({ + text = tr("actions.cycle"), + glyph = "refresh", + variant = "outline", + enabled = snapshot.busy ~= true, + onClick = activeTab == "output" and "onCycleOutput" or "onCycleInput", + }), + }) +end + +render = function() + local statusRows = {} + if snapshot.loading then + table.insert(statusRows, ui.label({ text = tr("panel.loading"), color = "primary" })) + end + if snapshot.busy then + table.insert(statusRows, ui.label({ text = tr("panel.switching"), color = "primary" })) + end + if tostring(snapshot.error or "") ~= "" then + table.insert(statusRows, ui.label({ text = snapshot.error, color = "error", maxLines = 2 })) + end + if feedback ~= "" then + table.insert(statusRows, ui.label({ text = feedback, color = feedbackError and "error" or "secondary", maxLines = 2 })) + end + + panel.render(ui.column({ flexGrow = 1, gap = 12, align = "stretch" }, { + header(), + ui.row({ gap = 12, align = "stretch" }, { volumeCard("output"), volumeCard("input") }), + toolbar(), + ui.column({ gap = 3 }, statusRows), + ui.scroll({ flexGrow = 1, gap = 6 }, { deviceList(activeTab) }), + ui.label({ + text = tr("panel.keybind_hint"), + fontSize = 10, + color = "on_surface_variant", + maxLines = 2, + }), + })) +end + +function onOpen(_context) + feedback = "" + feedbackError = false + sendCommand("refresh") + render() +end + +function onConfigChanged() + render() +end + +function onCloseClicked() panel.close() end +function onRefresh() sendCommand("refresh") end +function onScanBluetooth() sendCommand("scan_bluetooth") end +function onCycleOutput() sendCommand("cycle_output") end +function onCycleInput() sendCommand("cycle_input") end +function onOpenSettingsClicked() + local started = noctalia.runAsync("noctalia msg settings-open plugins", function(result) + if result.timedOut == true or tonumber(result.exitCode) ~= 0 then + noctalia.notifyError(tr("title"), tr("errors.command_start")) + end + end, 5000) + if not started then + noctalia.notifyError(tr("title"), tr("errors.command_start")) + end +end + +function onShowOutputs() + activeTab = "output" + editingId = nil + showHidden = false + render() +end + +function onShowInputs() + activeTab = "input" + editingId = nil + showHidden = false + render() +end + +function onToggleHidden() + showHidden = not showHidden + editingId = nil + render() +end + +function onCancelEdit() + editingId = nil + render() +end + +function onEditAliasChange(value) editAlias = tostring(value or "") end +function onEditIconChange(index, _label) + editIconStyle = ICON_STYLE_VALUES[(math.floor(tonumber(index) or 0)) + 1] or "automatic" +end +function onEditSlotChange(value) editSlot = tostring(value or "") end + +function onSaveEdit() + local devices = activeTab == "output" and asArray(snapshot.outputs) or asArray(snapshot.inputs) + for _, device in ipairs(devices) do + if device.id == editingId then + sendCommand("update_device", { + id = device.id, + address = device.address, + alias = editAlias, + iconStyle = editIconStyle, + slot = editSlot, + }) + editingId = nil + render() + return + end + end +end + +function onOutputVolumeChange(value) + outputVolumeCommitValue = math.floor(tonumber(value) or 0) +end + +function onInputVolumeChange(value) + inputVolumeCommitValue = math.floor(tonumber(value) or 0) +end + +-- Noctalia's slider onDragEnd callback has no arguments. Keep the latest +-- onChange value for the commit without using it as controlled render state. +function onOutputVolumeCommit() + local value = outputVolumeCommitValue or snapshot.outputVolume + outputVolumeCommitValue = nil + sendCommand("set_output_volume", { value = value }) +end +function onInputVolumeCommit() + local value = inputVolumeCommitValue or snapshot.inputVolume + inputVolumeCommitValue = nil + sendCommand("set_input_volume", { value = value }) +end +function onToggleOutputMute() sendCommand("toggle_output_mute") end +function onToggleInputMute() sendCommand("toggle_input_mute") end + +noctalia.state.watch(SNAPSHOT_KEY, function(value) + if type(value) == "table" then + snapshot = value + render() + end +end) + +noctalia.state.watch(RESULT_KEY, function(result) + if type(result) ~= "table" or not tostring(result.requestId or ""):match("^panel%-") then return end + feedback = tostring(result.message or "") + feedbackError = result.ok ~= true + if result.ok ~= true and (result.action == "set_output_volume" or result.action == "set_input_volume") then + outputVolumeCommitValue = nil + inputVolumeCommitValue = nil + -- A failed optimistic edit may have moved the native slider while the + -- controlled snapshot stayed unchanged. Recreate both controls so their + -- visual values are seeded from the authoritative snapshot again. + volumeControlRevision += 1 + end + render() +end) + +render() diff --git a/audio-switcher/plugin.toml b/audio-switcher/plugin.toml new file mode 100644 index 0000000..901233b --- /dev/null +++ b/audio-switcher/plugin.toml @@ -0,0 +1,44 @@ +id = "blackbartblues/audio-switcher" +name = "Audio Switcher" +version = "0.1.0" +plugin_api = 5 +author = "blackbartblues" +license = "MIT" +icon = "switch-horizontal" +description = "Switch audio inputs and outputs, hand off Bluetooth devices, and control them from compositor keybinds." +tags = ["audio", "bar", "hardware", "panel", "service", "system", "utility"] +dependencies = ["pactl", "bluetoothctl", "sleep"] + +[[setting]] +key = "show_percentage" +type = "bool" +label_key = "settings.show_percentage.label" +description_key = "settings.show_percentage.description" +default = true + +[[setting]] +key = "scroll_step" +type = "int" +label_key = "settings.scroll_step.label" +description_key = "settings.scroll_step.description" +default = 5 +min = 1 +max = 25 +step = 1 + +[[widget]] +id = "widget" +entry = "widget.luau" + +[[panel]] +id = "audio-switcher" +entry = "panel.luau" +width = 620 +height = 650 +placement = "attached" +position = "top_right" +open_near_click = true + +[[service]] +id = "service" +entry = "service.luau" diff --git a/audio-switcher/screenshots/panel.webp b/audio-switcher/screenshots/panel.webp new file mode 100644 index 0000000000000000000000000000000000000000..014e86123eb72417143df5f1ecda34f932a26293 GIT binary patch literal 20580 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