Merge branch 'master' into next
This commit is contained in:
64
NEWS.rst
64
NEWS.rst
@@ -1,6 +1,62 @@
|
|||||||
WirePlumber 0.4.14
|
WirePlumber 0.4.15
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
|
||||||
|
- A new "DSP policy" module has been added; its purpose is to automatically
|
||||||
|
load a filter-chain when a certain hardware device is present, so that
|
||||||
|
audio always goes through this software DSP before reaching the device.
|
||||||
|
This is mainly to support Apple M1/M2 devices, which require a software
|
||||||
|
DSP to be always present
|
||||||
|
|
||||||
|
- WpImplModule now supports loading module arguments directly from a SPA-JSON
|
||||||
|
config file; this is mainly to support DSP configuration for Apple M1/M2
|
||||||
|
and will likely be reworked for 0.5
|
||||||
|
|
||||||
|
- Added support for automatically combining Bluetooth LE Audio device sets
|
||||||
|
(e.g. pairs of earbuds) (!500)
|
||||||
|
|
||||||
|
- Added command line options in ``wpctl`` to display device/node names and
|
||||||
|
nicknames instead of descriptions
|
||||||
|
|
||||||
|
- Added zsh completions file for ``wpctl``
|
||||||
|
|
||||||
|
- The device profile selection policy now respects the ``device.profile``
|
||||||
|
property if it is set on the device; this is useful to hand-pick a profile
|
||||||
|
based on static configuration rules (alsa_monitor.rules)
|
||||||
|
|
||||||
|
Changes/Fixes:
|
||||||
|
|
||||||
|
- Linking policy now sends an error to the client before destroying the node,
|
||||||
|
if it determines that the node cannot be linked to any target; this fixes
|
||||||
|
error reporting on the client side
|
||||||
|
|
||||||
|
- Fixed a crash in suspend-node that could happen when destroying virtual
|
||||||
|
sinks that were loaded from another process such as pw-loopback (#467)
|
||||||
|
|
||||||
|
- Virtual machine default period size has been bumped to 1024 (#507)
|
||||||
|
|
||||||
|
- Updated bluez5 default configuration, using ``bluez5.roles`` instead of
|
||||||
|
``bluez5.headset-roles`` now (!498)
|
||||||
|
|
||||||
|
- Disabled Bluetooth autoconnect by default (!514)
|
||||||
|
|
||||||
|
- Removed ``RestrictNamespaces`` option from the systemd services in order to
|
||||||
|
allow libcamera to load sandboxed IPA modules (#466)
|
||||||
|
|
||||||
|
- Fixed a JSON encoding bug with empty strings (#471)
|
||||||
|
|
||||||
|
- Lua code can now parse strings without quotes from SPA-JSON
|
||||||
|
|
||||||
|
- Added some missing `\since` annotations and made them show up in the
|
||||||
|
generated gobject-introspection file, to help bindings generators
|
||||||
|
|
||||||
|
Past releases
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
WirePlumber 0.4.14
|
||||||
|
..................
|
||||||
|
|
||||||
Additions:
|
Additions:
|
||||||
|
|
||||||
- Added support for managing Bluetooth-MIDI, complimenting the parts that
|
- Added support for managing Bluetooth-MIDI, complimenting the parts that
|
||||||
@@ -24,9 +80,6 @@ Additions:
|
|||||||
- Added support for disabling libcamera nodes & devices with ``node.disabled``
|
- Added support for disabling libcamera nodes & devices with ``node.disabled``
|
||||||
and ``device.disabled``, like it works for ALSA and V4L2 (#418)
|
and ``device.disabled``, like it works for ALSA and V4L2 (#418)
|
||||||
|
|
||||||
Past releases
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WirePlumber 0.4.13
|
WirePlumber 0.4.13
|
||||||
..................
|
..................
|
||||||
|
|
||||||
@@ -76,9 +129,6 @@ Packaging:
|
|||||||
|
|
||||||
- Added pkg-config and header information in the gir file
|
- Added pkg-config and header information in the gir file
|
||||||
|
|
||||||
Past releases
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WirePlumber 0.4.12
|
WirePlumber 0.4.12
|
||||||
..................
|
..................
|
||||||
|
|
||||||
|
@@ -439,10 +439,24 @@ on the ALSA device.
|
|||||||
|
|
||||||
This can be done in 3 different ways:
|
This can be done in 3 different ways:
|
||||||
|
|
||||||
1. Use pavucontrol and toggle the codecs in the output advanced section
|
1. Use pavucontrol and toggle the codecs in the output advanced section.
|
||||||
|
|
||||||
2. Modify the ``["iec958.codecs"] = "[ PCM DTS AC3 MPEG MPEG2-AAC EAC3 TrueHD DTS-HD ]"``
|
2. Modify the ``["iec958.codecs"]`` node property to contain suported codecs.
|
||||||
node property to something.
|
|
||||||
|
Example ``~/.config/wireplumber/main.lua.d/51-alsa-spdif.lua``:
|
||||||
|
|
||||||
|
.. code-block:: lua
|
||||||
|
|
||||||
|
table.insert (alsa_monitor.rules, {
|
||||||
|
matches = {
|
||||||
|
{
|
||||||
|
{ "node.name", "matches", "alsa_output.*" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apply_properties = {
|
||||||
|
["iec958.codecs"] = "[ PCM DTS AC3 EAC3 TrueHD DTS-HD ]",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
|
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
|
||||||
the codecs at runtime.
|
the codecs at runtime.
|
||||||
|
@@ -11,7 +11,7 @@ dropped, the module is unloaded.
|
|||||||
Constructors
|
Constructors
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
.. function:: LocalModule(name, arguments, properties)
|
.. function:: LocalModule(name, arguments, properties, [load_args_from_file])
|
||||||
|
|
||||||
Loads the named module with the provided arguments and properties (either of
|
Loads the named module with the provided arguments and properties (either of
|
||||||
which can be ``nil``).
|
which can be ``nil``).
|
||||||
@@ -21,6 +21,7 @@ Constructors
|
|||||||
module arguments
|
module arguments
|
||||||
:param table properties: can be ``nil`` or a table that can be
|
:param table properties: can be ``nil`` or a table that can be
|
||||||
:ref:`converted <lua_gobject_lua_to_c>` to :c:struct:`WpProperties`
|
:ref:`converted <lua_gobject_lua_to_c>` to :c:struct:`WpProperties`
|
||||||
|
:param load_args_from_file: (since 0.4.15) optional. if true, arguments param is treated as a file path to load args from
|
||||||
:returns: a new LocalModule
|
:returns: a new LocalModule
|
||||||
:rtype: LocalModule (:c:struct:`WpImplModule`)
|
:rtype: LocalModule (:c:struct:`WpImplModule`)
|
||||||
:since: 0.4.2
|
:since: 0.4.2
|
||||||
|
@@ -10,6 +10,10 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
#include <pipewire/impl.h>
|
#include <pipewire/impl.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-module")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-module")
|
||||||
|
|
||||||
@@ -261,3 +265,63 @@ wp_impl_module_load (WpCore * core, const gchar * name,
|
|||||||
|
|
||||||
return module;
|
return module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Loads a PipeWire module with arguments from file into the WirePlumber process
|
||||||
|
*
|
||||||
|
* \ingroup wpimplmodule
|
||||||
|
* \since 0.4.15
|
||||||
|
* \param core (transfer none): The WirePlumber core
|
||||||
|
* \param name (transfer none): the name of the module to load
|
||||||
|
* \param filename (transfer none): filename to be used as arguments
|
||||||
|
* \param properties (nullable) (transfer none): additional properties to be
|
||||||
|
* provided to the module
|
||||||
|
* \returns (nullable) (transfer full): the WpImplModule for the module that
|
||||||
|
* was loaded on success, %NULL on failure.
|
||||||
|
*/
|
||||||
|
WpImplModule *
|
||||||
|
wp_impl_module_load_file (WpCore * core, const gchar * name,
|
||||||
|
const gchar * filename, WpProperties * properties)
|
||||||
|
{
|
||||||
|
char *config = "";
|
||||||
|
int fd = open(filename, O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
g_warning("Failed to open config file %s: %m", filename);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat stats;
|
||||||
|
int err = fstat(fd, &stats);
|
||||||
|
if (err < 0) {
|
||||||
|
g_warning("Failed to stat config file %s: %m", filename);
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
config = mmap(NULL, stats.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
||||||
|
if (config == MAP_FAILED){
|
||||||
|
g_warning("Failed to mmap config file %s: %m", filename);
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
WpImplModule *module = WP_IMPL_MODULE (
|
||||||
|
g_object_new (WP_TYPE_IMPL_MODULE,
|
||||||
|
"core", core,
|
||||||
|
"name", name,
|
||||||
|
"arguments", config,
|
||||||
|
"properties", properties,
|
||||||
|
NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
munmap(config, stats.st_size);
|
||||||
|
|
||||||
|
if (!module->pw_impl_module) {
|
||||||
|
/* Module loading failed, free and return */
|
||||||
|
g_object_unref (module);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
@@ -29,6 +29,9 @@ G_DECLARE_FINAL_TYPE (WpImplModule, wp_impl_module, WP, IMPL_MODULE, GObject);
|
|||||||
WP_API
|
WP_API
|
||||||
WpImplModule * wp_impl_module_load (WpCore * core, const gchar * name,
|
WpImplModule * wp_impl_module_load (WpCore * core, const gchar * name,
|
||||||
const gchar * arguments, WpProperties * properties);
|
const gchar * arguments, WpProperties * properties);
|
||||||
|
WP_API
|
||||||
|
WpImplModule * wp_impl_module_load_file (WpCore * core, const gchar * name,
|
||||||
|
const gchar * filename, WpProperties * properties);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
|
@@ -638,8 +638,8 @@ wp_object_manager_is_interested_in_global (WpObjectManager * self,
|
|||||||
/* and consider the manager interested if the type and the globals match...
|
/* and consider the manager interested if the type and the globals match...
|
||||||
if pw_properties / g_properties fail, that's ok because they are not
|
if pw_properties / g_properties fail, that's ok because they are not
|
||||||
known yet (the proxy is likely NULL and properties not yet retrieved) */
|
known yet (the proxy is likely NULL and properties not yet retrieved) */
|
||||||
if (match & (WP_INTEREST_MATCH_GTYPE |
|
if (SPA_FLAG_IS_SET (match, (WP_INTEREST_MATCH_GTYPE |
|
||||||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES)) {
|
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES))) {
|
||||||
gpointer ft = g_hash_table_lookup (self->features,
|
gpointer ft = g_hash_table_lookup (self->features,
|
||||||
GSIZE_TO_POINTER (global->type));
|
GSIZE_TO_POINTER (global->type));
|
||||||
*wanted_features = (WpObjectFeatures) GPOINTER_TO_UINT (ft);
|
*wanted_features = (WpObjectFeatures) GPOINTER_TO_UINT (ft);
|
||||||
@@ -1049,18 +1049,15 @@ wp_registry_detach (WpRegistry *self)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static gboolean
|
||||||
expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
|
expose_tmp_globals (WpCore *core)
|
||||||
{
|
{
|
||||||
g_autoptr (GError) error = NULL;
|
WpRegistry *self = wp_core_get_registry (core);
|
||||||
g_autoptr (GPtrArray) tmp_globals = NULL;
|
g_autoptr (GPtrArray) tmp_globals = NULL;
|
||||||
|
|
||||||
if (!wp_core_sync_finish (core, res, &error))
|
|
||||||
wp_warning_object (core, "core sync error: %s", error->message);
|
|
||||||
|
|
||||||
/* in case the registry was cleared in the meantime... */
|
/* in case the registry was cleared in the meantime... */
|
||||||
if (G_UNLIKELY (!self->tmp_globals))
|
if (G_UNLIKELY (!self->tmp_globals))
|
||||||
return;
|
return G_SOURCE_REMOVE;
|
||||||
|
|
||||||
/* steal the tmp_globals list and replace it with an empty one */
|
/* steal the tmp_globals list and replace it with an empty one */
|
||||||
tmp_globals = self->tmp_globals;
|
tmp_globals = self->tmp_globals;
|
||||||
@@ -1084,8 +1081,8 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
|
|||||||
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_return_if_fail (self->globals->len <= g->id ||
|
g_return_val_if_fail (self->globals->len <= g->id ||
|
||||||
g_ptr_array_index (self->globals, g->id) == NULL);
|
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
|
||||||
|
|
||||||
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
||||||
g->registry = self;
|
g->registry = self;
|
||||||
@@ -1111,6 +1108,8 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
|
|||||||
}
|
}
|
||||||
wp_object_manager_maybe_objects_changed (om);
|
wp_object_manager_maybe_objects_changed (om);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1161,7 +1160,8 @@ wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
|||||||
|
|
||||||
/* schedule exposing when adding the first global */
|
/* schedule exposing when adding the first global */
|
||||||
if (self->tmp_globals->len == 1) {
|
if (self->tmp_globals->len == 1) {
|
||||||
wp_core_sync (core, NULL, (GAsyncReadyCallback) expose_tmp_globals, self);
|
wp_core_idle_add_closure (core, NULL,
|
||||||
|
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* store the most permissive permissions */
|
/* store the most permissive permissions */
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
#include <glib/gstdio.h>
|
#include <glib/gstdio.h>
|
||||||
#include <wp/wp.h>
|
#include <wp/wp.h>
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
@@ -1536,8 +1537,20 @@ impl_module_new (lua_State *L)
|
|||||||
properties = wplua_table_to_properties (L, 3);
|
properties = wplua_table_to_properties (L, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
|
bool load_file = false; // Load args as file path
|
||||||
name, args, properties);
|
if (lua_type (L, 4) != LUA_TNONE && lua_type (L, 4) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 4, LUA_TBOOLEAN);
|
||||||
|
load_file = lua_toboolean(L, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
WpImplModule *m = NULL;
|
||||||
|
if (load_file) {
|
||||||
|
m = wp_impl_module_load_file (get_wp_export_core (L),
|
||||||
|
name, args, properties);
|
||||||
|
} else {
|
||||||
|
m = wp_impl_module_load (get_wp_export_core (L),
|
||||||
|
name, args, properties);
|
||||||
|
}
|
||||||
|
|
||||||
if (m) {
|
if (m) {
|
||||||
wplua_pushobject (L, m);
|
wplua_pushobject (L, m);
|
||||||
|
@@ -35,13 +35,17 @@ monitor.alsa.midi.node-properties = {
|
|||||||
|
|
||||||
## Removes longname/number from MIDI port names
|
## Removes longname/number from MIDI port names
|
||||||
api.alsa.disable-longname = true
|
api.alsa.disable-longname = true
|
||||||
|
|
||||||
|
## Set priorities so that it can be used as a fallback driver (see pipewire#3562)
|
||||||
|
priority.session = 100
|
||||||
|
priority.driver = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.alsa.vm.node.defaults = {
|
monitor.alsa.vm.node.defaults = {
|
||||||
## These properties override node defaults when running in a virtual machine.
|
## These properties override node defaults when running in a virtual machine.
|
||||||
## The rules below still override those.
|
## The rules below still override those.
|
||||||
|
|
||||||
api.alsa.period-size = 256
|
api.alsa.period-size = 1024
|
||||||
api.alsa.headroom = 8192
|
api.alsa.headroom = 8192
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,6 +26,8 @@ SimpleEventHook {
|
|||||||
local off_profile = nil
|
local off_profile = nil
|
||||||
local best_profile = nil
|
local best_profile = nil
|
||||||
local unk_profile = nil
|
local unk_profile = nil
|
||||||
|
-- Takes absolute priority if available or unknown
|
||||||
|
local profile_prop = device.properties["device.profile"]
|
||||||
|
|
||||||
-- skip hook if profile is already selected
|
-- skip hook if profile is already selected
|
||||||
if selected_profile then
|
if selected_profile then
|
||||||
@@ -34,7 +36,9 @@ SimpleEventHook {
|
|||||||
|
|
||||||
for p in device:iterate_params ("EnumProfile") do
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
profile = cutils.parseParam (p, "EnumProfile")
|
profile = cutils.parseParam (p, "EnumProfile")
|
||||||
if profile and profile.name ~= "pro-audio" then
|
if profile and profile.name == profile_prop and profile.available ~= "no" then
|
||||||
|
selected_profile = profile
|
||||||
|
elseif profile and profile.name ~= "pro-audio" then
|
||||||
if profile.name == "off" then
|
if profile.name == "off" then
|
||||||
off_profile = profile
|
off_profile = profile
|
||||||
elseif profile.available == "yes" then
|
elseif profile.available == "yes" then
|
||||||
|
61
src/scripts/policy-dsp.lua
Normal file
61
src/scripts/policy-dsp.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022-2023 The WirePlumber project contributors
|
||||||
|
-- @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
local config = ... or {}
|
||||||
|
config.rules = config.rules or {}
|
||||||
|
|
||||||
|
for _, r in ipairs(config.rules) do
|
||||||
|
r.interests = {}
|
||||||
|
for _, i in ipairs(r.matches) do
|
||||||
|
local interest_desc = { type = "properties" }
|
||||||
|
|
||||||
|
for _, c in ipairs(i) do
|
||||||
|
c.type = "pw"
|
||||||
|
table.insert(interest_desc, Constraint(c))
|
||||||
|
end
|
||||||
|
|
||||||
|
local interest = Interest(interest_desc)
|
||||||
|
table.insert(r.interests, interest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: only check for hotplug of nodes with known DSP rules
|
||||||
|
nodes_om = ObjectManager {
|
||||||
|
Interest { type = "node" },
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_chains = {}
|
||||||
|
|
||||||
|
nodes_om:connect("object-added", function (om, node)
|
||||||
|
for _, r in ipairs(config.rules or {}) do
|
||||||
|
for _, interest in ipairs(r.interests) do
|
||||||
|
if interest:matches(node["global-properties"]) then
|
||||||
|
local id = node["global-properties"]["object.id"]
|
||||||
|
|
||||||
|
if r.filter_chain then
|
||||||
|
if filter_chains[id] then
|
||||||
|
Log.warning("Sink " .. id .. " has been plugged now, but has a filter chain loaded. Skipping")
|
||||||
|
else
|
||||||
|
filter_chains[id] = LocalModule("libpipewire-module-filter-chain", r.filter_chain, {}, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
nodes_om:connect("object-removed", function (om, node)
|
||||||
|
local id = node["global-properties"]["object.id"]
|
||||||
|
if filter_chains[id] then
|
||||||
|
Log.debug("Unloading filter chain associated with sink " .. id)
|
||||||
|
filter_chains[id] = nil
|
||||||
|
else
|
||||||
|
Log.debug("Disconnected sink " .. id .. " does not have any filters to be removed")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
nodes_om:activate()
|
@@ -8,6 +8,11 @@ executable('wpctl',
|
|||||||
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
|
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
install_data('shell-completion/wpctl.zsh',
|
||||||
|
install_dir: get_option('datadir') / 'zsh/site-functions',
|
||||||
|
rename: '_wpctl'
|
||||||
|
)
|
||||||
|
|
||||||
executable('wpexec',
|
executable('wpexec',
|
||||||
'wpexec.c',
|
'wpexec.c',
|
||||||
c_args : [
|
c_args : [
|
||||||
|
49
src/tools/shell-completion/wpctl.zsh
Normal file
49
src/tools/shell-completion/wpctl.zsh
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#compdef wpctl
|
||||||
|
|
||||||
|
(( $+functions[_wpctl_pw_nodes] )) ||
|
||||||
|
_wpctl_pw_nodes() {
|
||||||
|
local -a pw_objects
|
||||||
|
if (( $+commands[pw-dump] )) && (( $+commands[jq] )); then
|
||||||
|
local -a pw_objects=(${(@f)"$(2>/dev/null {
|
||||||
|
command pw-dump |
|
||||||
|
command jq -r '.[] | select(
|
||||||
|
.type == "PipeWire:Interface:Node"
|
||||||
|
) |
|
||||||
|
{id, type, name: (
|
||||||
|
.info.name //
|
||||||
|
(.info.props | (
|
||||||
|
."application.name" //
|
||||||
|
."node.name")
|
||||||
|
) //
|
||||||
|
.type)
|
||||||
|
} |
|
||||||
|
"\(.id):\(.name | gsub(":"; "\\:"))"'
|
||||||
|
})"})
|
||||||
|
fi
|
||||||
|
_wpctl_describe_nodes() {_describe "node id" pw_objects "$@"}
|
||||||
|
_alternative \
|
||||||
|
'pw-defaults:defaults:(@DEFAULT_SINK@ @DEFAULT_SOURCE@)' \
|
||||||
|
'pw-node-id:node id:_wpctl_describe_nodes'
|
||||||
|
}
|
||||||
|
|
||||||
|
local -a node_id=(/$'[^\0]#\0'/ ':pw-node-id:node id:_wpctl_pw_nodes')
|
||||||
|
local -a volume=(/$'[0-9]##(%|)([+-]|)\0'/ ':volume:volume:( )')
|
||||||
|
local -a toggle=(/$'[^\0]#\0'/ ':(0 1 toggle)')
|
||||||
|
local -a set_volume=( "$node_id[@]" "$volume[@]" )
|
||||||
|
local -a set_mute=( "$node_id[@]" "$toggle[@]" )
|
||||||
|
|
||||||
|
_regex_words options 'wpctl options' \
|
||||||
|
{-h,--help}':show help message and exit'
|
||||||
|
local -a options=( "$reply[@]" )
|
||||||
|
|
||||||
|
_regex_words wpctl-commands 'wpctl commands' \
|
||||||
|
'status:show wireplumber status' \
|
||||||
|
'get-volume:get object volume:$node_id' \
|
||||||
|
'set-default:set a default sink:$node_id' \
|
||||||
|
'set-volume:set object volume:$set_volume' \
|
||||||
|
'set-mute:set object mute:$set_mute' \
|
||||||
|
'set-profile:set object profile:$node_id' \
|
||||||
|
'clear-default:unset default sink:$node_id'
|
||||||
|
local -a wpctlcmd=( /$'[^\0]#\0'/ "$options[@]" "#" "$reply[@]")
|
||||||
|
_regex_arguments _wpctl "$wpctlcmd[@]"
|
||||||
|
_wpctl "$@"
|
Reference in New Issue
Block a user