summaryrefslogtreecommitdiff
path: root/pactl-widget
diff options
context:
space:
mode:
authorstreetturtle <streetturtle@users.noreply.github.com>2023-01-18 13:32:47 -0500
committerGitHub <noreply@github.com>2023-01-18 13:32:47 -0500
commita3f4a0731696f62cce4b715dcd1e5975ff553439 (patch)
tree74fa1e2dcf5ddbc9cdd5f8cff149c597d8ce524b /pactl-widget
parent5f251902cf2b5cc25e54e2ce3b6292a741d52399 (diff)
parent81d725fe84ba96f689ad1e4e8990c7750bc40d33 (diff)
Merge pull request #386 from shuber2/pactl-widget
pactl: A new volume widget using pactl only
Diffstat (limited to 'pactl-widget')
-rw-r--r--pactl-widget/README.md54
-rw-r--r--pactl-widget/pactl.lua124
-rw-r--r--pactl-widget/utils.lua28
-rw-r--r--pactl-widget/volume.lua233
4 files changed, 439 insertions, 0 deletions
diff --git a/pactl-widget/README.md b/pactl-widget/README.md
new file mode 100644
index 0000000..24e4471
--- /dev/null
+++ b/pactl-widget/README.md
@@ -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.
diff --git a/pactl-widget/pactl.lua b/pactl-widget/pactl.lua
new file mode 100644
index 0000000..638dc7e
--- /dev/null
+++ b/pactl-widget/pactl.lua
@@ -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
diff --git a/pactl-widget/utils.lua b/pactl-widget/utils.lua
new file mode 100644
index 0000000..52e7869
--- /dev/null
+++ b/pactl-widget/utils.lua
@@ -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
diff --git a/pactl-widget/volume.lua b/pactl-widget/volume.lua
new file mode 100644
index 0000000..53441fd
--- /dev/null
+++ b/pactl-widget/volume.lua
@@ -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 })