From 25d9eecfc68df3251dc96008aaa4cd7c81900da6 Mon Sep 17 00:00:00 2001 From: streetturtle Date: Fri, 19 Mar 2021 20:49:00 -0400 Subject: [volume] BREAKING CHANGE - new widget instead of old ones Having three widgets for volume led to a problem of code duplication - same logic was duplicated three times. However when an issue was discovered and fixed, it was fixed in only one of three widgets. So I decided to create a volume widget from scratch, adding new features, such as selecting input/output, better responsiveness, easily customizable widget ui (bar, text, icon, icon and text, arc). Should close #199, #198, #185, #182, #47, #122, #183. --- volume-widget/volume.lua | 329 ++++++++++++++++++++++++++--------------------- 1 file changed, 182 insertions(+), 147 deletions(-) (limited to 'volume-widget/volume.lua') diff --git a/volume-widget/volume.lua b/volume-widget/volume.lua index cb8c21d..59f0a7a 100644 --- a/volume-widget/volume.lua +++ b/volume-widget/volume.lua @@ -1,181 +1,216 @@ ------------------------------------------------- --- Volume Widget for Awesome Window Manager --- Shows the current volume level +-- The Ultimate Volume Widget for Awesome Window Manager -- More details could be found here: -- https://github.com/streetturtle/awesome-wm-widgets/tree/master/volume-widget --- @author Pavel Makhov, Aurélien Lajoie --- @copyright 2018 Pavel Makhov +-- @author Pavel Makhov +-- @copyright 2020 Pavel Makhov ------------------------------------------------- +local awful = require("awful") local wibox = require("wibox") local spawn = require("awful.spawn") -local naughty = require("naughty") -local gfs = require("gears.filesystem") -local dpi = require('beautiful').xresources.apply_dpi - -local PATH_TO_ICONS = "/usr/share/icons/Arc/status/symbolic/" -local volume_icon_name="audio-volume-high-symbolic" -local GET_VOLUME_CMD = 'amixer sget Master' - -local volume = { - device = '', - display_notification = false, - display_notification_onClick = true, - notification = nil, - delta = 5 -} +local gears = require("gears") +local beautiful = require("beautiful") +local watch = require("awful.widget.watch") +local utils = require("awesome-wm-widgets.volume-widget.utils") -function volume:toggle() - volume:_cmd('amixer ' .. volume.device .. ' sset Master toggle') -end -function volume:raise() - volume:_cmd('amixer ' .. volume.device .. ' sset Master ' .. tostring(volume.delta) .. '%+') -end -function volume:lower() - volume:_cmd('amixer ' .. volume.device .. ' sset Master ' .. tostring(volume.delta) .. '%-') -end +local LIST_DEVICES_CMD = [[sh -c "pacmd list-sinks; pacmd list-sources"]] +local GET_VOLUME_CMD = 'amixer -D pulse sget Master' +local INC_VOLUME_CMD = 'amixer -D pulse sset Master 5%+' +local DEC_VOLUME_CMD = 'amixer -D pulse sset Master 5%-' +local TOG_VOLUME_CMD = 'amixer -D pulse sset Master toggle' ---{{{ Icon and notification update - --------------------------------------------------- --- Set the icon and return the message to display --- base on sound level and mute --------------------------------------------------- -local function parse_output(stdout) - local level = string.match(stdout, "(%d?%d?%d)%%") - if stdout:find("%[off%]") then - volume_icon_name="audio-volume-muted-symbolic_red" - return level.."% Mute" - end - level = tonumber(string.format("% 3d", level)) - - if (level >= 0 and level < 25) then - volume_icon_name="audio-volume-muted-symbolic" - elseif (level < 50) then - volume_icon_name="audio-volume-low-symbolic" - elseif (level < 75) then - volume_icon_name="audio-volume-medium-symbolic" + +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.properties.device_description .. ' · ' .. device.ports[device.active_port] else - volume_icon_name="audio-volume-high-symbolic" + return device.properties.device_description end - return level.."%" end --------------------------------------------------------- ---Update the icon and the notification if needed --------------------------------------------------------- -local function update_graphic(widget, stdout, _, _, _) - local txt = parse_output(stdout) - widget.image = PATH_TO_ICONS .. volume_icon_name .. ".svg" - if (volume.display_notification or volume.display_notification_onClick) then - volume.notification.iconbox.image = PATH_TO_ICONS .. volume_icon_name .. ".svg" - naughty.replace_text(volume.notification, "Volume", txt) - 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() + spawn.easy_async(string.format([[sh -c 'pacmd set-default-%s "%s"']], device_type, device.name), function() + on_checkbox_click() + end) + end) -local function notif(msg, keep) - if (volume.display_notification or (keep and volume.display_notification_onClick)) then - naughty.destroy(volume.notification) - volume.notification= naughty.notify{ - text = msg, - icon=PATH_TO_ICONS .. volume_icon_name .. ".svg", - icon_size = dpi(16), - title = "Volume", - position = volume.position, - timeout = keep and 0 or 2, hover_timeout = 0.5, - width = 200, - screen = mouse.screen + 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() + spawn.easy_async(string.format([[sh -c 'pacmd set-default-%s "%s"']], device_type, device.name), function() + on_checkbox_click() + end) + end) + + table.insert(device_rows, row) end + + return device_rows +end + +local function build_header_row(text) + return wibox.widget{ + { + markup = "" .. text .. "", + align = 'center', + widget = wibox.widget.textbox + }, + bg = beautiful.bg_normal, + widget = wibox.container.background + } +end + +local function rebuild_popup() + spawn.easy_async(LIST_DEVICES_CMD, function(stdout) + + local sinks, sources = utils.extract_sinks_and_sources(stdout) + + for i = 0, #rows do rows[i]=nil end + + 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) end ---}}} local function worker(user_args) ---{{{ Args + local args = user_args or {} - local volume_audio_controller = args.volume_audio_controller or 'pulse' - volume.display_notification = args.display_notification or false - volume.display_notification_onClick = args.display_notification_onClick or true - volume.position = args.notification_position or "top_right" - if volume_audio_controller == 'pulse' then - volume.device = '-D pulse' - end - volume.delta = args.delta or 5 - GET_VOLUME_CMD = 'amixer ' .. volume.device.. ' sget Master' ---}}} ---{{{ Check for icon path - if not gfs.dir_readable(PATH_TO_ICONS) then - naughty.notify{ - title = "Volume Widget", - text = "Folder with icons doesn't exist: " .. PATH_TO_ICONS, - preset = naughty.config.presets.critical - } - return + local widget_type = args.widget_type + local refresh_rate = args.refresh_rate or 1 + + if widget_types[widget_type] == nil then + volume.widget = widget_types['icon_and_text'].get_widget(user_args.icon_and_text_args) + else + volume.widget = widget_types[widget_type].get_widget(args) end ---}}} ---{{{ Widget creation - volume.widget = wibox.widget { - { - id = "icon", - image = PATH_TO_ICONS .. "audio-volume-muted-symbolic.svg", - resize = false, - widget = wibox.widget.imagebox, - }, - margins = 3, - layout = wibox.container.margin, - set_image = function(self, path) - self.icon.image = path + + local function update_graphic(widget, stdout) + local mute = string.match(stdout, "%[(o%D%D?)%]") -- \[(o\D\D?)\] - [on] or [off] + if mute == 'off' then widget:mute() + elseif mute == 'on' then widget:unmute() end - } ---}}} ---{{{ Spawn functions - function volume:_cmd(cmd) - notif("") - spawn.easy_async(cmd, function(stdout, stderr, exitreason, exitcode) - update_graphic(volume.widget, stdout, stderr, exitreason, exitcode) - end) + local volume_level = string.match(stdout, "(%d?%d?%d)%%") -- (\d?\d?\d)\%) + volume_level = string.format("% 3d", volume_level) + widget:set_volume_level(volume_level) end - local function show() - spawn.easy_async(GET_VOLUME_CMD, function(stdout, _, _, _) - local txt = parse_output(stdout) - notif(txt, true) - end - ) + function volume:inc() + spawn.easy_async(INC_VOLUME_CMD, function(stdout) update_graphic(volume.widget, stdout) end) end ---}}} ---{{{ Mouse event - --[[ allows control volume level by: - - clicking on the widget to mute/unmute - - scrolling when cursor is over the widget - ]] - volume.widget:connect_signal("button::press", function(_,_,_,button) - if (button == 4) then volume.raise() - elseif (button == 5) then volume.lower() - elseif (button == 1) then volume.toggle() - end - end) - if volume.display_notification then - volume.widget:connect_signal("mouse::enter", function() show() end) - volume.widget:connect_signal("mouse::leave", function() naughty.destroy(volume.notification) end) - elseif volume.display_notification_onClick then - volume.widget:connect_signal("button::press", function(_,_,_,button) - if (button == 3) then show() end - end) - volume.widget:connect_signal("mouse::leave", function() naughty.destroy(volume.notification) end) + + function volume:dec() + spawn.easy_async(DEC_VOLUME_CMD, function(stdout) update_graphic(volume.widget, stdout) end) end ---}}} ---{{{ Set initial icon - spawn.easy_async(GET_VOLUME_CMD, function(stdout) - parse_output(stdout) - volume.widget.image = PATH_TO_ICONS .. volume_icon_name .. ".svg" - end) ---}}} + function volume:toggle() + spawn.easy_async(TOG_VOLUME_CMD, function(stdout) update_graphic(volume.widget, stdout) end) + end + + volume.widget:buttons( + awful.util.table.join( + awful.button({}, 3, function() + if popup.visible then + popup.visible = not popup.visible + else + rebuild_popup() + popup:move_next_to(mouse.current_widget_geometry) + end + end), + awful.button({}, 4, function() volume:inc() end), + awful.button({}, 5, function() volume:dec() end), + awful.button({}, 1, function() volume:toggle() end) + ) + ) + + watch(GET_VOLUME_CMD, refresh_rate, update_graphic, volume.widget) return volume.widget end -- cgit v1.2.3