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:
Stefano Mazzucco 2017-05-25 14:41:27 +01:00
parent b04347033f
commit 5d9df5db5f
5 changed files with 284 additions and 144 deletions

117
README.md
View File

@ -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).

View File

@ -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"
}
}

View File

@ -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"},
}

View File

@ -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()

View File

@ -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()