Add dynamic volume/sink updates: use dbus_proxy and Awesome 4.x
- use Awesome 4.x API. This widget will not work with earlier versions. - use the latest pulseaudio_dbus (v0.10.0) backed by dbus_proxy. - use client script to listen to signals from pulseaudio to update the widget status. - update copyright notice - refactor widget code - update the documentation - add rockspec for development - add rockspec for version 0.2.0
This commit is contained in:
parent
b04347033f
commit
5d9df5db5f
117
README.md
117
README.md
|
@ -1,27 +1,36 @@
|
|||
# A widget for the Awesome Window Manager to control the volume
|
||||
# A widget for the Awesome Window Manager 4.x to control the volume
|
||||
|
||||
This widget is a wrapper around the
|
||||
[`pulseaudio_dbus`](https://luarocks.org/modules/stefano-m/pulseaudio_dbus)
|
||||
library for the Awesome Window Manager.
|
||||
A widget for the Awesome Window Manager (version 4.x) that
|
||||
uses [pulseaudio_dbus](https://github.com/stefano-m/lua-pulseaudio_dbus) to
|
||||
control your audio devices.
|
||||
|
||||
## A note about PulseAudio, DBus and Awesome
|
||||
|
||||
The Pulseaudio DBus interface requires clients to use peer-to-peer connection
|
||||
rather than the usual system/session buses. This means that we *cannot* use the
|
||||
Awesome DBus API that supports *only* system and session buses.
|
||||
|
||||
The solution is to run an external client application to establish a
|
||||
peer-to-peer connection and listen to DBus signals. The output of the client is
|
||||
read by the widget that updates itself accordingly. This is done thanks
|
||||
to
|
||||
[`awful.spawn.with_line_callback`](https://awesomewm.org/apidoc/libraries/awful.spawn.html#with_line_callback).
|
||||
|
||||
# Requirements
|
||||
|
||||
In addition to the requirements listed in the `rockspec` file, you will need
|
||||
the [Awesome Window Manager](https://awesomewm.org)
|
||||
and PulseAudio with DBus enabled (for more information about this, see the
|
||||
[`pulseaudio_dbus`](https://luarocks.org/modules/stefano-m/pulseaudio_dbus)
|
||||
documentation).
|
||||
the [Awesome Window Manager](https://awesomewm.org) *version 4.x* and
|
||||
PulseAudio with DBus enabled.
|
||||
|
||||
You will also need the DBus headers (`dbus.h`) installed.
|
||||
For example, Debian and Ubuntu provide the DBus headers with the `libdbus-1-dev`
|
||||
package, Fedora, RedHad and CentOS provide them with the `dbus-devel` package,
|
||||
while Arch provides them (alongside the binaries) with the `libdbus` package.
|
||||
To enable DBus in PulseAudio, ensure that the line
|
||||
|
||||
load-module module-dbus-protocol
|
||||
|
||||
is present in `/etc/pulse/default.pa` or `~/.config/pulse/default.pa`
|
||||
|
||||
# Installation
|
||||
|
||||
## Using Luarocks
|
||||
|
||||
Probably, the easiest way to install this widget is to use `luarocks`:
|
||||
The easiest way to install this widget is to use `luarocks`:
|
||||
|
||||
luarocks install pulseaudio_widget
|
||||
|
||||
|
@ -30,29 +39,11 @@ it system-wide
|
|||
|
||||
This will ensure that all its dependencies are installed.
|
||||
|
||||
### A note about ldbus
|
||||
|
||||
This module depends on the [`ldbus`](https://github.com/daurnimator/ldbus)
|
||||
module that provides the low-level DBus bindings
|
||||
|
||||
luarocks install --server=http://luarocks.org/manifests/daurnimator \
|
||||
ldbus \
|
||||
DBUS_INCDIR=/usr/include/dbus-1.0/ \
|
||||
DBUS_ARCH_INCDIR=/usr/lib/dbus-1.0/include
|
||||
|
||||
As usual, you can use the `--local` option if you don't want or can't install
|
||||
it system-wide.
|
||||
|
||||
## From source
|
||||
|
||||
Alternatively, you can copy the `pulseaudio_widget.lua` file in your
|
||||
`~/.config/awesome` folder. You will have to install all the dependencies
|
||||
manually though (see the `rockspec` file for more information).
|
||||
|
||||
# Configuration
|
||||
|
||||
The widget displays volume icons that are searched in the folder defined
|
||||
by `beautiful.pulse_icon_theme` with extension `beautiful.pulse_icon_extension`.
|
||||
The widget displays volume icons that are searched in the folder defined by
|
||||
`beautiful.pulse_icon_theme` with extension `beautiful.pulse_icon_extension`.
|
||||
The default is to look into `"/usr/share/icons/Adwaita/scalable/status"` for
|
||||
icons whose extension is `".svg"`.
|
||||
|
||||
|
@ -69,47 +60,45 @@ When the widget is focused:
|
|||
|
||||
* Scroll: controls the volume
|
||||
* Left button: toggles mute
|
||||
* Right button: launches mixer (defaults to `pavucontrol`)
|
||||
* Right button: launches mixer (`mixer` field of the widget table, defaults to
|
||||
`pavucontrol`)
|
||||
|
||||
# Usage
|
||||
|
||||
Add the following to your `~/.config/awesome/rc.lua`:
|
||||
|
||||
Require the module:
|
||||
|
||||
-- require *after* `beautiful.init` or the theme will be inconsistent!
|
||||
local pulse = require("pulseaudio_widget")
|
||||
``` lua
|
||||
-- require *after* `beautiful.init` or the theme will be inconsistent!
|
||||
local pulse = require("pulseaudio_widget")
|
||||
|
||||
```
|
||||
|
||||
Add the widget to your layout:
|
||||
|
||||
right_layout:add(pulse)
|
||||
``` lua
|
||||
s.mywibox:setup {
|
||||
layout = wibox.layout.align.horizontal,
|
||||
{ -- Left widgets },
|
||||
s.mytasklist, -- Middle widget
|
||||
{ -- Right widgets
|
||||
pulse
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally add some keyboard shortcuts to control the volume:
|
||||
|
||||
awful.util.table.join(
|
||||
awful.key({ }, "XF86AudioRaiseVolume", pulse.volume_up),
|
||||
awful.key({ }, "XF86AudioLowerVolume", pulse.volume_down),
|
||||
awful.key({ }, "XF86AudioMute", pulse.toggle_muted)
|
||||
)
|
||||
|
||||
# Limitations
|
||||
|
||||
This widget cannot show when headphones are plugged or unplugged.
|
||||
|
||||
(Un)plugging headphones will result in Pulseaudio's current Sink to
|
||||
change its "active" port and issue an "ActivePortUpdated" signal.
|
||||
However, there is no way for Awesome to detect such signal because
|
||||
its DBus API can connect only to session and system buses because,
|
||||
unfortunately, pulseaudio uses peer-to-peer connections (i.e. it opens
|
||||
a specific socket owned by the current user).
|
||||
|
||||
This is unfortunate because it's quite handy to have e.g. the muted
|
||||
speakers and unmuted headphones.
|
||||
|
||||
That said, the widget will continue to work and update the volume and
|
||||
mute state of both. It will just not show the actual status when the
|
||||
headphones are unplugged.
|
||||
``` lua
|
||||
awful.util.table.join(
|
||||
awful.key({ }, "XF86AudioRaiseVolume", pulse.volume_up),
|
||||
awful.key({ }, "XF86AudioLowerVolume", pulse.volume_down),
|
||||
awful.key({ }, "XF86AudioMute", pulse.toggle_muted)
|
||||
)
|
||||
```
|
||||
|
||||
# Credits
|
||||
|
||||
Although heavily modified, this program is derived from the
|
||||
[Awesome Pulseaudio Widget (APW)](https://github.com/mokasin/apw).
|
||||
This program was inspired by
|
||||
the [Awesome Pulseaudio Widget (APW)](https://github.com/mokasin/apw).
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package = "pulseaudio_widget"
|
||||
version = "0.2.0-1"
|
||||
source = {
|
||||
url = "git://github.com/stefano-m/awesome-pulseaudio_widget",
|
||||
tag = "v0.2.0"
|
||||
}
|
||||
description = {
|
||||
summary = "A PulseAudio widget for the Awesome Window Manager",
|
||||
detailed = [[
|
||||
Control your audio in the Awesome with PulseAudio and DBus.
|
||||
]],
|
||||
homepage = "https://github.com/stefano-m/awesome-pulseaudio_widget",
|
||||
license = "GPL v3"
|
||||
}
|
||||
supported_platforms = {
|
||||
"linux"
|
||||
}
|
||||
dependencies = {
|
||||
"lua >= 5.1",
|
||||
"pulseaudio_dbus >= 0.10.0, < 0.11"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
pulseaudio_widget = "pulseaudio_widget.lua",
|
||||
pulseaudio_widget_client = "pulseaudio_widget_client.lua"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package = "pulseaudio_widget"
|
||||
version = "devel-1"
|
||||
source = {
|
||||
url = "git://github.com/stefano-m/awesome-pulseaudio_widget",
|
||||
tag = "master"
|
||||
}
|
||||
description = {
|
||||
summary = "A PulseAudio widget for the Awesome Window Manager",
|
||||
detailed = [[
|
||||
Control your audio in the Awesome with PulseAudio and DBus.
|
||||
]],
|
||||
homepage = "https://github.com/stefano-m/awesome-pulseaudio_widget",
|
||||
license = "GPL v3"
|
||||
}
|
||||
dependencies = {
|
||||
"lua >= 5.1",
|
||||
"pulseaudio_dbus",
|
||||
}
|
||||
supported_platforms = { "linux" }
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = { pulseaudio_widget = "pulseaudio_widget.lua",
|
||||
pulseaudio_widget_client = "pulseaudio_widget_client.lua"},
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
--[[
|
||||
Copyright 2016 Stefano Mazzucco <stefano AT curso DOT re>
|
||||
Copyright 2013 mokasin
|
||||
Copyright 2017 Stefano Mazzucco <stefano AT curso DOT re>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,29 +14,28 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Although heavily modified, this program is derived from the
|
||||
This program was inspired by the
|
||||
[Awesome Pulseaudio Widget (APW)](https://github.com/mokasin/apw)
|
||||
]]
|
||||
|
||||
local awesome = awesome -- luacheck: ignore
|
||||
local string = string
|
||||
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local naughty = require("naughty")
|
||||
|
||||
local ldbus = require("ldbus_api")
|
||||
local pulse = require("pulseaudio_dbus")
|
||||
|
||||
local spawn_with_shell = awful.util.spawn_with_shell or awful.spawn.with_shell
|
||||
local icon_theme = "/usr/share/icons/Adwaita/scalable/status"
|
||||
local icon_extension = ".svg"
|
||||
|
||||
icon_theme = beautiful.pulse_icon_theme or icon_theme
|
||||
icon_extension = beautiful.pulse_icon_extension or icon_extension
|
||||
|
||||
local widget = wibox.widget.imagebox()
|
||||
local widget_t = awful.tooltip({ objects = { widget },})
|
||||
|
||||
local icon = {
|
||||
high = icon_theme .. "/audio-volume-high-symbolic" .. icon_extension,
|
||||
med = icon_theme .. "/audio-volume-medium-symbolic" .. icon_extension,
|
||||
|
@ -45,108 +43,128 @@ local icon = {
|
|||
muted = icon_theme .. "/audio-volume-muted-symbolic" .. icon_extension
|
||||
}
|
||||
|
||||
local status, address = pcall(pulse.get_address)
|
||||
if not status then
|
||||
naughty.notify({title="Error while loading PulseAudio",
|
||||
text=address,
|
||||
preset=naughty.config.presets.critical})
|
||||
return widget
|
||||
end
|
||||
local widget = wibox.widget.imagebox()
|
||||
widget.tooltip = awful.tooltip({ objects = { widget },})
|
||||
|
||||
pulse.listen_for_signal(address, "org.PulseAudio.Core1", "NewSink")
|
||||
local watcher = ldbus.api.watch(address)
|
||||
function widget:update_appearance(v)
|
||||
local i, msg
|
||||
|
||||
local function _get_volume_as_string()
|
||||
local volume = {}
|
||||
|
||||
for _, v in ipairs(widget.sink.volume) do
|
||||
volume[v] = 0
|
||||
end
|
||||
|
||||
local msg = ""
|
||||
for k, _ in pairs(volume) do
|
||||
msg = msg .. k .. "%"
|
||||
end
|
||||
|
||||
return msg
|
||||
end
|
||||
|
||||
local function _update_sink_if_changed()
|
||||
local sink_added = watcher()
|
||||
if sink_added ~= "no_answer" then
|
||||
local new_sink = assert(pulse.get_sinks(address)[1])
|
||||
widget.sink = pulse.Sink:new(address, new_sink)
|
||||
end
|
||||
end
|
||||
|
||||
local function _update_appearance()
|
||||
-- Get first channel only.
|
||||
local v = widget.sink.volume[1]
|
||||
local i
|
||||
if widget.sink.muted then
|
||||
if v == "Muted" then
|
||||
msg = v
|
||||
i = icon.muted
|
||||
elseif v <= 33 then
|
||||
i = icon.low
|
||||
elseif v <= 66 then
|
||||
i = icon.med
|
||||
else
|
||||
i = icon.high
|
||||
v = v == "Unmuted" and self.sink:get_volume_percent()[1] or tonumber(v)
|
||||
msg = string.format("%d%%", v)
|
||||
if v <= 33 then
|
||||
i = icon.low
|
||||
elseif v <= 66 then
|
||||
i = icon.med
|
||||
else
|
||||
i = icon.high
|
||||
end
|
||||
end
|
||||
|
||||
widget:set_image(i)
|
||||
widget_t:set_text(_get_volume_as_string())
|
||||
self:set_image(i)
|
||||
self.tooltip:set_text(msg)
|
||||
|
||||
end
|
||||
|
||||
local function _init()
|
||||
local first_sink = assert(pulse.get_sinks(address)[1])
|
||||
widget.mixer = "pavucontrol"
|
||||
widget.sink = pulse.Sink:new(address, first_sink)
|
||||
_update_appearance()
|
||||
function widget.notify(v)
|
||||
local msg = tonumber(v) and string.format("%d%%", v) or v
|
||||
naughty.notify({text=msg, timeout=1})
|
||||
end
|
||||
|
||||
local function _notify_volume()
|
||||
naughty.notify({
|
||||
text='Volume: ' .. _get_volume_as_string(),
|
||||
timeout=1,
|
||||
})
|
||||
function widget:update_sink(object_path)
|
||||
self.sink = pulse.get_sink(self.connection, object_path)
|
||||
end
|
||||
|
||||
function widget.volume_up()
|
||||
_update_sink_if_changed()
|
||||
if not widget.sink.muted then
|
||||
if not widget.sink:is_muted() then
|
||||
widget.sink:volume_up()
|
||||
_update_appearance()
|
||||
_notify_volume()
|
||||
end
|
||||
end
|
||||
|
||||
function widget.volume_down()
|
||||
_update_sink_if_changed()
|
||||
if not widget.sink.muted then
|
||||
if not widget.sink:is_muted() then
|
||||
widget.sink:volume_down()
|
||||
_update_appearance()
|
||||
_notify_volume()
|
||||
end
|
||||
end
|
||||
|
||||
function widget.toggle_muted()
|
||||
_update_sink_if_changed()
|
||||
widget.sink:toggle_muted()
|
||||
_update_appearance()
|
||||
end
|
||||
|
||||
function widget.launch_mixer()
|
||||
spawn_with_shell(widget.mixer)
|
||||
function widget:kill_client()
|
||||
if type(self.server_pid) == "number" then
|
||||
awful.spawn("kill -TERM " .. self.server_pid)
|
||||
end
|
||||
end
|
||||
|
||||
-- register mouse button actions
|
||||
widget:buttons(awful.util.table.join(
|
||||
function widget:run_client()
|
||||
|
||||
local pid = awful.spawn.with_line_callback(
|
||||
[[lua -e 'require("pulseaudio_widget_client")']],
|
||||
{
|
||||
stdout = function (line)
|
||||
local v, found, _
|
||||
|
||||
v, found = line:gsub("^(VolumeUpdated:%s+)(%d)", "%2")
|
||||
if found ~= 0 then
|
||||
self:update_appearance(v)
|
||||
widget.notify(v)
|
||||
end
|
||||
|
||||
v, found = line:gsub("^(MuteUpdated:%s+)(%w)", "%2")
|
||||
if found ~= 0 then
|
||||
self:update_appearance(v)
|
||||
widget.notify(v)
|
||||
end
|
||||
|
||||
v, found = line:gsub("^(NewSink:%s+)(/.*%w)", "%2")
|
||||
if found ~=0 then
|
||||
self:update_sink(v)
|
||||
local volume = self.sink:get_volume_percent()[1]
|
||||
self:update_appearance(volume)
|
||||
widget.notify(volume)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
self.server_pid = pid
|
||||
end
|
||||
|
||||
widget:buttons(gears.table.join(
|
||||
awful.button({ }, 1, widget.toggle_muted),
|
||||
awful.button({ }, 3, widget.launch_mixer),
|
||||
awful.button({ }, 3, function () awful.spawn(widget.mixer) end),
|
||||
awful.button({ }, 4, widget.volume_up),
|
||||
awful.button({ }, 5, widget.volume_down)))
|
||||
|
||||
-- initialize
|
||||
_init()
|
||||
awesome.connect_signal("exit", function () widget:kill_client() end)
|
||||
|
||||
return widget
|
||||
function widget:init()
|
||||
local status, address = pcall(pulse.get_address)
|
||||
if not status then
|
||||
naughty.notify({title="Error while loading the PulseAudio widget",
|
||||
text=address,
|
||||
preset=naughty.config.presets.critical})
|
||||
return self
|
||||
end
|
||||
|
||||
self.mixer = "pavucontrol"
|
||||
|
||||
self.connection = pulse.get_connection(address)
|
||||
self.core = pulse.get_core(self.connection)
|
||||
local sink_path = assert(self.core:get_sinks()[1], "No sinks found")
|
||||
|
||||
self:update_sink(sink_path)
|
||||
local volume = self.sink:get_volume_percent()[1]
|
||||
self:update_appearance(volume)
|
||||
|
||||
self:run_client()
|
||||
|
||||
self.__index = self
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return widget:init()
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
--[[
|
||||
Copyright 2017 Stefano Mazzucco <stefano AT curso DOT re>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
]]
|
||||
|
||||
--[[-- This module is meant to be run with
|
||||
[`awful.spawn.with_line_callback`](https://awesomewm.org/apidoc/libraries/awful.spawn.html#with_line_callback).
|
||||
|
||||
It starts a client that listens to the pulseaudio DBus server and prints to
|
||||
standard output wheter the volume or sinks (e.g. selecting the audio from the
|
||||
TV) change. The changes are printed to standard output and are used by the
|
||||
pulseaudio widget to change its appearance.
|
||||
|
||||
We must do this because Awesome's DBus API can only connect to system and
|
||||
session buses, but pulseaudio uses its own per-user connection.
|
||||
|
||||
]]
|
||||
|
||||
local pulse = require("pulseaudio_dbus")
|
||||
local GLib = require("lgi").GLib
|
||||
|
||||
local address = pulse.get_address()
|
||||
|
||||
local connection = pulse.get_connection(address)
|
||||
local core = pulse.get_core(connection)
|
||||
local sink = pulse.get_sink(connection, core.Sinks[1])
|
||||
|
||||
-- listen on ALL objects as sinks may change
|
||||
core:ListenForSignal("org.PulseAudio.Core1.Device.VolumeUpdated", {})
|
||||
core:ListenForSignal("org.PulseAudio.Core1.Device.MuteUpdated", {})
|
||||
|
||||
local function connect_sink(s)
|
||||
if s.signals.VolumeUpdated then
|
||||
s:connect_signal(
|
||||
function (self, vols)
|
||||
local v = math.ceil(tonumber(vols[1][1]) / self.BaseVolume * 100)
|
||||
print(string.format("VolumeUpdated: %s", v))
|
||||
end,
|
||||
"VolumeUpdated"
|
||||
)
|
||||
end
|
||||
|
||||
if s.signals.MuteUpdated then
|
||||
s:connect_signal(
|
||||
function (_, is_mute)
|
||||
local m = is_mute[1] and "Muted" or "Unmuted"
|
||||
print(string.format("MuteUpdated: %s", m))
|
||||
end,
|
||||
"MuteUpdated"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
connect_sink(sink)
|
||||
|
||||
core:ListenForSignal("org.PulseAudio.Core1.NewSink", {core.object_path})
|
||||
core:connect_signal(
|
||||
function (_, newsinks)
|
||||
print(string.format("NewSink: %s", newsinks[1]))
|
||||
sink = pulse.get_sink(connection, newsinks[1])
|
||||
connect_sink(sink)
|
||||
end,
|
||||
"NewSink"
|
||||
)
|
||||
|
||||
-- Start the client. Send SIGTERM to stop it.
|
||||
print("Starting Awesome PulseAudio Widget Client")
|
||||
GLib.MainLoop():run()
|
Loading…
Reference in New Issue