Merge pull request #386 from shuber2/pactl-widget
pactl: A new volume widget using pactl only
This commit is contained in:
commit
a3f4a07316
|
@ -0,0 +1,54 @@
|
|||
# Pactl volume widget
|
||||
|
||||
This is a volume widget that uses `pactl` only for controlling volume and
|
||||
selecting sinks and sources. Hence, it can be used with PulseAudio or PipeWire
|
||||
likewise, unlike the original Volume widget.
|
||||
|
||||
Other than that it is heavily based on the original widget, including its
|
||||
customization and icon options. For screenshots, see the original widget.
|
||||
|
||||
## Installation
|
||||
|
||||
Clone the repo under **~/.config/awesome/** and add widget in **rc.lua**:
|
||||
|
||||
```lua
|
||||
local volume_widget = require('awesome-wm-widgets.pactl-widget.volume')
|
||||
...
|
||||
s.mytasklist, -- Middle widget
|
||||
{ -- Right widgets
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
...
|
||||
-- default
|
||||
volume_widget(),
|
||||
-- customized
|
||||
volume_widget{
|
||||
widget_type = 'arc'
|
||||
},
|
||||
```
|
||||
|
||||
### Shortcuts
|
||||
|
||||
To improve responsiveness of the widget when volume level is changed by a shortcut use corresponding methods of the widget:
|
||||
|
||||
```lua
|
||||
awful.key({}, "XF86AudioRaiseVolume", function () volume_widget:inc(5) end),
|
||||
awful.key({}, "XF86AudioLowerVolume", function () volume_widget:dec(5) end),
|
||||
awful.key({}, "XF86AudioMute", function () volume_widget:toggle() end),
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
It is possible to customize the widget by providing a table with all or some of
|
||||
the following config parameters:
|
||||
|
||||
### Generic parameter
|
||||
|
||||
| Name | Default | Description |
|
||||
|---|---|---|
|
||||
| `mixer_cmd` | `pavucontrol` | command to run on middle click (e.g. a mixer program) |
|
||||
| `step` | `5` | How much the volume is raised or lowered at once (in %) |
|
||||
| `widget_type`| `icon_and_text`| Widget type, one of `horizontal_bar`, `vertical_bar`, `icon`, `icon_and_text`, `arc` |
|
||||
| `device` | `@DEFAULT_SINK@` | Select the device name to control |
|
||||
|
||||
For more details on parameters depending on the chosen widget type, please
|
||||
refer to the original Volume widget.
|
|
@ -0,0 +1,124 @@
|
|||
local spawn = require("awful.spawn")
|
||||
local utils = require("awesome-wm-widgets.pactl-widget.utils")
|
||||
|
||||
local pactl = {}
|
||||
|
||||
|
||||
function pactl.volume_increase(device, step)
|
||||
spawn('pactl set-sink-volume ' .. device .. ' +' .. step .. '%', false)
|
||||
end
|
||||
|
||||
function pactl.volume_decrease(device, step)
|
||||
spawn('pactl set-sink-volume ' .. device .. ' -' .. step .. '%', false)
|
||||
end
|
||||
|
||||
function pactl.mute_toggle(device)
|
||||
spawn('pactl set-sink-mute ' .. device .. ' toggle', false)
|
||||
end
|
||||
|
||||
function pactl.get_volume(device)
|
||||
local stdout = utils.popen_and_return('pactl get-sink-volume ' .. device)
|
||||
|
||||
local volsum, volcnt = 0, 0
|
||||
for vol in string.gmatch(stdout, "(%d?%d?%d)%%") do
|
||||
vol = tonumber(vol)
|
||||
if vol ~= nil then
|
||||
volsum = volsum + vol
|
||||
volcnt = volcnt + 1
|
||||
end
|
||||
end
|
||||
|
||||
if volcnt == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return volsum / volcnt
|
||||
end
|
||||
|
||||
function pactl.get_mute(device)
|
||||
local stdout = utils.popen_and_return('pactl get-sink-mute ' .. device)
|
||||
if string.find(stdout, "yes") then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function pactl.get_sinks_and_sources()
|
||||
local default_sink = utils.trim(utils.popen_and_return('pactl get-default-sink'))
|
||||
local default_source = utils.trim(utils.popen_and_return('pactl get-default-source'))
|
||||
|
||||
local sinks = {}
|
||||
local sources = {}
|
||||
|
||||
local device
|
||||
local ports
|
||||
local key
|
||||
local value
|
||||
local in_section
|
||||
|
||||
for line in utils.popen_and_return('pactl list'):gmatch('[^\r\n]*') do
|
||||
|
||||
if string.match(line, '^%a+ #') then
|
||||
in_section = nil
|
||||
end
|
||||
|
||||
local is_sink_line = string.match(line, '^Sink #')
|
||||
local is_source_line = string.match(line, '^Source #')
|
||||
|
||||
if is_sink_line or is_source_line then
|
||||
in_section = "main"
|
||||
|
||||
device = {
|
||||
id = line:match('#(%d+)'),
|
||||
is_default = false
|
||||
}
|
||||
if is_sink_line then
|
||||
table.insert(sinks, device)
|
||||
else
|
||||
table.insert(sources, device)
|
||||
end
|
||||
end
|
||||
|
||||
-- Found a new subsection
|
||||
if in_section ~= nil and string.match(line, '^\t%a+:$') then
|
||||
in_section = utils.trim(line):lower()
|
||||
in_section = string.sub(in_section, 1, #in_section-1)
|
||||
|
||||
if in_section == 'ports' then
|
||||
ports = {}
|
||||
device['ports'] = ports
|
||||
end
|
||||
end
|
||||
|
||||
-- Found a key-value pair
|
||||
if string.match(line, "^\t*[^\t]+: ") then
|
||||
local t = utils.split(line, ':')
|
||||
key = utils.trim(t[1]):lower():gsub(' ', '_')
|
||||
value = utils.trim(t[2])
|
||||
end
|
||||
|
||||
-- Key value pair on 1st level
|
||||
if in_section ~= nil and string.match(line, "^\t[^\t]+: ") then
|
||||
device[key] = value
|
||||
|
||||
if key == "name" and (value == default_sink or value == default_source) then
|
||||
device['is_default'] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Key value pair in ports section
|
||||
if in_section == "ports" and string.match(line, "^\t\t[^\t]+: ") then
|
||||
ports[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return sinks, sources
|
||||
end
|
||||
|
||||
function pactl.set_default(type, name)
|
||||
spawn('pactl set-default-' .. type .. ' "' .. name .. '"', false)
|
||||
end
|
||||
|
||||
|
||||
return pactl
|
|
@ -0,0 +1,28 @@
|
|||
local utils = {}
|
||||
|
||||
|
||||
function utils.trim(str)
|
||||
return string.match(str, "^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
function utils.split(string_to_split, separator)
|
||||
if separator == nil then separator = "%s" end
|
||||
local t = {}
|
||||
|
||||
for str in string.gmatch(string_to_split, "([^".. separator .."]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
function utils.popen_and_return(cmd)
|
||||
local handle = io.popen(cmd)
|
||||
local result = handle:read("*a")
|
||||
handle:close()
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
return utils
|
|
@ -0,0 +1,233 @@
|
|||
-------------------------------------------------
|
||||
-- A purely pactl-based volume widget based on the original Volume widget
|
||||
-- More details could be found here:
|
||||
-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/pactl-widget
|
||||
|
||||
-- @author Stefan Huber
|
||||
-- @copyright 2023 Stefan Huber
|
||||
-------------------------------------------------
|
||||
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local spawn = require("awful.spawn")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local pactl = require("awesome-wm-widgets.pactl-widget.pactl")
|
||||
local utils = require("awesome-wm-widgets.pactl-widget.utils")
|
||||
|
||||
|
||||
local widget_types = {
|
||||
icon_and_text = require("awesome-wm-widgets.volume-widget.widgets.icon-and-text-widget"),
|
||||
icon = require("awesome-wm-widgets.volume-widget.widgets.icon-widget"),
|
||||
arc = require("awesome-wm-widgets.volume-widget.widgets.arc-widget"),
|
||||
horizontal_bar = require("awesome-wm-widgets.volume-widget.widgets.horizontal-bar-widget"),
|
||||
vertical_bar = require("awesome-wm-widgets.volume-widget.widgets.vertical-bar-widget")
|
||||
}
|
||||
local volume = {}
|
||||
|
||||
local rows = { layout = wibox.layout.fixed.vertical }
|
||||
|
||||
local popup = awful.popup{
|
||||
bg = beautiful.bg_normal,
|
||||
ontop = true,
|
||||
visible = false,
|
||||
shape = gears.shape.rounded_rect,
|
||||
border_width = 1,
|
||||
border_color = beautiful.bg_focus,
|
||||
maximum_width = 400,
|
||||
offset = { y = 5 },
|
||||
widget = {}
|
||||
}
|
||||
|
||||
local function build_main_line(device)
|
||||
if device.active_port ~= nil and device.ports[device.active_port] ~= nil then
|
||||
return device.description .. ' · ' .. utils.split(device.ports[device.active_port], " ")[1]
|
||||
else
|
||||
return device.description
|
||||
end
|
||||
end
|
||||
|
||||
local function build_rows(devices, on_checkbox_click, device_type)
|
||||
local device_rows = { layout = wibox.layout.fixed.vertical }
|
||||
for _, device in pairs(devices) do
|
||||
|
||||
local checkbox = wibox.widget {
|
||||
checked = device.is_default,
|
||||
color = beautiful.bg_normal,
|
||||
paddings = 2,
|
||||
shape = gears.shape.circle,
|
||||
forced_width = 20,
|
||||
forced_height = 20,
|
||||
check_color = beautiful.fg_urgent,
|
||||
widget = wibox.widget.checkbox
|
||||
}
|
||||
|
||||
checkbox:connect_signal("button::press", function()
|
||||
pactl.set_default(device_type, device.name)
|
||||
on_checkbox_click()
|
||||
end)
|
||||
|
||||
local row = wibox.widget {
|
||||
{
|
||||
{
|
||||
{
|
||||
checkbox,
|
||||
valign = 'center',
|
||||
layout = wibox.container.place,
|
||||
},
|
||||
{
|
||||
{
|
||||
text = build_main_line(device),
|
||||
align = 'left',
|
||||
widget = wibox.widget.textbox
|
||||
},
|
||||
left = 10,
|
||||
layout = wibox.container.margin
|
||||
},
|
||||
spacing = 8,
|
||||
layout = wibox.layout.align.horizontal
|
||||
},
|
||||
margins = 4,
|
||||
layout = wibox.container.margin
|
||||
},
|
||||
bg = beautiful.bg_normal,
|
||||
widget = wibox.container.background
|
||||
}
|
||||
|
||||
row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
|
||||
row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)
|
||||
|
||||
local old_cursor, old_wibox
|
||||
row:connect_signal("mouse::enter", function()
|
||||
local wb = mouse.current_wibox
|
||||
old_cursor, old_wibox = wb.cursor, wb
|
||||
wb.cursor = "hand1"
|
||||
end)
|
||||
row:connect_signal("mouse::leave", function()
|
||||
if old_wibox then
|
||||
old_wibox.cursor = old_cursor
|
||||
old_wibox = nil
|
||||
end
|
||||
end)
|
||||
|
||||
row:connect_signal("button::press", function()
|
||||
pactl.set_default(device_type, device.name)
|
||||
on_checkbox_click()
|
||||
end)
|
||||
|
||||
table.insert(device_rows, row)
|
||||
end
|
||||
|
||||
return device_rows
|
||||
end
|
||||
|
||||
local function build_header_row(text)
|
||||
return wibox.widget{
|
||||
{
|
||||
markup = "<b>" .. text .. "</b>",
|
||||
align = 'center',
|
||||
widget = wibox.widget.textbox
|
||||
},
|
||||
bg = beautiful.bg_normal,
|
||||
widget = wibox.container.background
|
||||
}
|
||||
end
|
||||
|
||||
local function rebuild_popup()
|
||||
for i = 0, #rows do
|
||||
rows[i]=nil
|
||||
end
|
||||
|
||||
local sinks, sources = pactl.get_sinks_and_sources()
|
||||
table.insert(rows, build_header_row("SINKS"))
|
||||
table.insert(rows, build_rows(sinks, function() rebuild_popup() end, "sink"))
|
||||
table.insert(rows, build_header_row("SOURCES"))
|
||||
table.insert(rows, build_rows(sources, function() rebuild_popup() end, "source"))
|
||||
|
||||
popup:setup(rows)
|
||||
end
|
||||
|
||||
local function worker(user_args)
|
||||
|
||||
local args = user_args or {}
|
||||
|
||||
local mixer_cmd = args.mixer_cmd or 'pavucontrol'
|
||||
local widget_type = args.widget_type
|
||||
local refresh_rate = args.refresh_rate or 1
|
||||
local step = args.step or 5
|
||||
local device = args.device or '@DEFAULT_SINK@'
|
||||
|
||||
if widget_types[widget_type] == nil then
|
||||
volume.widget = widget_types['icon_and_text'].get_widget(args.icon_and_text_args)
|
||||
else
|
||||
volume.widget = widget_types[widget_type].get_widget(args)
|
||||
end
|
||||
|
||||
local function update_graphic(widget)
|
||||
local vol = pactl.get_volume(device)
|
||||
if vol ~= nil then
|
||||
widget:set_volume_level(vol)
|
||||
end
|
||||
|
||||
if pactl.get_mute(device) then
|
||||
widget:mute()
|
||||
else
|
||||
widget:unmute()
|
||||
end
|
||||
end
|
||||
|
||||
function volume:inc(s)
|
||||
pactl.volume_increase(device, s or step)
|
||||
update_graphic(volume.widget)
|
||||
end
|
||||
|
||||
function volume:dec(s)
|
||||
pactl.volume_decrease(device, s or step)
|
||||
update_graphic(volume.widget)
|
||||
end
|
||||
|
||||
function volume:toggle()
|
||||
pactl.mute_toggle(device)
|
||||
update_graphic(volume.widget)
|
||||
end
|
||||
|
||||
function volume:popup()
|
||||
if popup.visible then
|
||||
popup.visible = not popup.visible
|
||||
else
|
||||
rebuild_popup()
|
||||
popup:move_next_to(mouse.current_widget_geometry)
|
||||
end
|
||||
end
|
||||
|
||||
function volume:mixer()
|
||||
if mixer_cmd then
|
||||
spawn(mixer_cmd)
|
||||
end
|
||||
end
|
||||
|
||||
volume.widget:buttons(
|
||||
awful.util.table.join(
|
||||
awful.button({}, 1, function() volume:toggle() end),
|
||||
awful.button({}, 2, function() volume:mixer() end),
|
||||
awful.button({}, 3, function() volume:popup() end),
|
||||
awful.button({}, 4, function() volume:inc() end),
|
||||
awful.button({}, 5, function() volume:dec() end)
|
||||
)
|
||||
)
|
||||
|
||||
gears.timer {
|
||||
timeout = refresh_rate,
|
||||
call_now = true,
|
||||
autostart = true,
|
||||
callback = function()
|
||||
update_graphic(volume.widget)
|
||||
end
|
||||
}
|
||||
|
||||
return volume.widget
|
||||
end
|
||||
|
||||
|
||||
return setmetatable(volume, { __call = function(_, ...) return worker(...) end })
|
Loading…
Reference in New Issue