diff --git a/widgets/clientmenu/volume.lua b/widgets/clientmenu/volume.lua index 356d1bf..08be061 100644 --- a/widgets/clientmenu/volume.lua +++ b/widgets/clientmenu/volume.lua @@ -15,6 +15,53 @@ local beautiful = require("beautiful") local ask = require("asckey") local test_pactl = os.execute("pactl --version") +G_ClientSinksByPID = G_ClientSinksByPID or {} +G_ClientSinksByName = G_ClientSinksByName or {} +G_SinkVolumeLevels = G_SinkVolumeLevels or {} +G_SinkMediaTypes = G_SinkMediaTypes or {} +local update_client_volumes = function() + awful.spawn.easy_async("pactl -n \"awesome\" list sink-inputs",function(stdout) + local pactl_data = fastyaml(stdout) + local indexed_sinks = {} + local sinks_by_pid = {} + local sinks_by_name = {} + local sink_volume_levels = {} + local sink_media_types = {} + for _,v in pairs(pactl_data) do + local sink_id = tonumber(v:match("Sink Input #(%d+)")) + if sink_id then + if v:match("application.process.id = \"(%d+)\"") then + local pid = tonumber(v:match("application.process.id = \"(%d+)\"")) + sinks_by_pid[pid] = sinks_by_pid[pid] or {} + table.insert(sinks_by_pid[pid], sink_id) + indexed_sinks[sink_id] = true + end + if v:match("application.name = \"([^\n]+)\"") then + local name = v:match("application.name = \"([^\n]+)\"") + sinks_by_name[name] = sinks_by_name[name] or {} + if not indexed_sinks[sink_id] then + indexed_sinks[sink_id] = true + table.insert(sinks_by_name[name], sink_id) + end + end + if indexed_sinks[sink_id] then + sink_volume_levels[sink_id] = tonumber(v:match("Volume: .-(%d+)%%")) + sink_media_types[sink_id] = v:match("media.name = \"([^\"]+)\"") or + v:match("media.class = \"([^\"]+)\"") + end + end + end + G_ClientSinksByName = sinks_by_name + G_ClientSinksByPID = sinks_by_pid + G_SinkVolumeLevels = sink_volume_levels + G_SinkMediaTypes = sink_media_types + end) +end +G_ClientSinksUpdateTimer = G_ClientSinksUpdateTimer or gears.timer({ + timeout = 0.5, + autostart = true, + callback = update_client_volumes +}) local result = test_pactl if _VERSION:match("5.1") then result = (test_pactl == 0) @@ -38,117 +85,182 @@ end return function(args) local style = awmtk2.create_style("client_volume", - awmtk2.generic.oneline_widget, args.style,args.vertical) + awmtk2.generic.oneline_widget, args.style) local templates = awmtk2.create_template_lib("client_volume",awmtk2.templates,args.templates) - local t = awmtk2.build_templates(templates,style,args.vertical) + local t = awmtk2.build_templates(templates,style) local widget = wibox.widget(t.container({ - t.icon({ + { + t.icon({ + image = get_icon(0); + resize = true + }), + t.textbox({ + markup = "No sound/Not available" + }), + visible = true, + id = "error", + spacing = style.base.spacing, + layout = wibox.layout.fixed.horizontal + }, + { + id = "client_volume_container", + spacing = style.base.spacing, + layout = wibox.layout.fixed.vertical + }, + spacing = style.base.spacing, + layout = wibox.layout.fixed.vertical + })) + local client_volume_container = widget:get_children_by_id("client_volume_container")[1] + local errorbox = widget:get_children_by_id("error")[1] + local id_by_slider_container = {} + local active_sliders = {} + -- Asynchronous promise for a "create_slider" function + local create_slider = function(sink_input_id) end + local remove_slider = function(sink_input_id) + require('naughty').notify({title = "remove_slider called", text=tostring(sink_input_id)}) + local index_to_remove = nil + for k,v in pairs(client_volume_container.children) do + if id_by_slider_container[v] == sink_input_id then + index_to_remove = k + end + end + if index_to_remove then + active_sliders[sink_input_id] = nil + client_volume_container:remove(index_to_remove) + end + end + -- Callback to update all slider values + local function update_active_sliders() + local checked_sliders = {} + if client.focus and client.focus.name then + for _,v in pairs(G_ClientSinksByName[client.focus.name] or {}) do + checked_sliders[v] = true + if not active_sliders[v] then + create_slider(v) + end + end + end + if client.focus and client.focus.pid then + for _,v in pairs(G_ClientSinksByPID[client.focus.pid] or {}) do + checked_sliders[v] = true + if not active_sliders[v] then + create_slider(v) + end + end + end + for k,_ in pairs(active_sliders) do + if not checked_sliders[k] then + remove_slider(k) + end + end + for sink_input_id,slider in pairs(active_sliders) do + slider.value = G_SinkVolumeLevels[sink_input_id] or -1 + end + end + -- Update sliders every 0.5 seconds + local update_sliders = gears.timer({ + timeout = 0.5, + autostart = true, + callback = update_active_sliders + }) + -- Function to set client volume + local function volume(filter,value) + update_sliders:again() + if type(filter) == "number" then + awful.spawn("pactl set-sink-input-volume "..tostring(filter).." "..tostring(value).."%") + elseif filter then + if filter.name then + for _,v in pairs(G_ClientSinksByName[filter.name] or {}) do + awful.spawn("pactl set-sink-input-volume "..tostring(v).." "..tostring(value).."%") + end + end + if filter.pid then + for _,v in pairs(G_ClientSinksByPID[filter.pid] or {}) do + awful.spawn("pactl set-sink-input-volume "..tostring(v).." "..tostring(value).."%") + end + end + end + end + create_slider = function(sink_input_id) + require('naughty').notify({title = "create_slider called", text=tostring(sink_input_id)}) + local slider_icon_container = wibox.widget(t.icon({ id = "client_volume_icon", resize = true, - }), - (args.vertical and { - t.textbox({ - id = "error" - }), - widget = wibox.container.rotate, - direction = "east" - }) or t.textbox({ - id = "error" - }), - t.slider({ + })) + local slider_icon = slider_icon_container:get_children_by_id("client_volume_icon")[1] + local slider_container = wibox.widget(t.slider({ minimum = 0, maximum = 100, id = "client_volume", - value = -1 - }), - layout = (args.vertical and wibox.layout.fixed.vertical) or - wibox.layout.fixed.horizontal - })) - local errorbox = widget:get_children_by_id("error")[1] - local icon = widget:get_children_by_id("client_volume_icon")[1] - local slider = widget:get_children_by_id("client_volume")[1] - -- Local tracking value to prevent zero volume on start - local touched = false - -- Attach to focus change - client.connect_signal("update_volume",function(c) - awful.spawn.easy_async("pactl list sink-inputs",function(stdout) - local pactl_data = fastyaml(stdout) - local cl - for _,v in pairs(pactl_data) do - if not c then return end - if v:match("application.process.id = \""..tostring(c.pid).."\"") then - cl = v - end + value = -1, + })) + local slider = slider_container:get_children_by_id("client_volume")[1] + local slider_touching = false + slider:connect_signal("widget::redraw_needed",function() + if slider_touching then + volume(sink_input_id,slider.value) end - if not cl then - slider.visible = false - errorbox.visible = true - errorbox:set_markup("No sound/Not available") - icon:set_image(beautiful["volume-muted-symbolic"]) - return - end - local volume = tonumber(cl:match("Volume:[^\n]-(%d*)%%")) - slider.visible = true - errorbox.visible = false - icon:set_image(get_icon(volume)) - slider.value = volume - touched = true + slider_icon.image = get_icon(slider.value) end) - end) - client.connect_signal("focus",function(c) - touched = false - c:emit_signal("update_volume") - end) - local update_timer = gears.timer({ - timeout = 0.5, - autostart = true, - callback = function() - if client.focus then - client.focus:emit_signal("update_volume") - end - end - }) - -- Async lock to prevent callback interference - local volume_lock = false - -- Function to set client volume - local function volume(value) - if volume_lock then return end - volume_lock = true - awful.spawn.easy_async("pactl list sink-inputs",function(stdout) - local pactl_data = fastyaml(stdout) - if not (client.focus and client.focus.pid) then - volume_lock = false - return - end - for _,v in pairs(pactl_data) do - if v:match("application.process.id = \""..tostring(client.focus.pid).."\"") then - local sink_id = v:match("^%s*Sink Input #(%d+)") - if sink_id then - print(sink_id, value) - awful.spawn("pactl set-sink-input-volume "..tostring(sink_id).." "..tostring(value).."%") - end - end - end - volume_lock = false + active_sliders[sink_input_id] = slider + slider:connect_signal("mouse::enter", function() + slider_touching = true end) + slider:connect_signal("mouse::leave", function() + slider_touching = false + end) + local new_widget = wibox.widget({ + t.textbox({ + markup = G_SinkMediaTypes[sink_input_id], + ellipsize = "end", + forced_width = style.slider.width, + forced_height = style.slider.height*(2/3) + }), + { + slider_icon_container, + slider_container, + layout = wibox.layout.fixed.horizontal + }, + spacing = style.base.spacing, + layout = wibox.layout.fixed.vertical, + id = tostring(sink_input_id) + }) + client_volume_container:add(new_widget) + id_by_slider_container[new_widget] = sink_input_id end - -- Attach change to slider - slider:connect_signal("widget::redraw_needed",function() - if touched then - volume(slider.value) - update_timer:again() + local function update_slider_list(c) + active_sliders = {} + client_volume_container:reset() + update_sliders:again() + local count = false + if c.name then + for _,v in pairs(G_ClientSinksByName[c.name] or {}) do + create_slider(v) + count = true + end end - end) + if c.pid then + for _,v in pairs(G_ClientSinksByPID[c.pid] or {}) do + create_slider(v) + count = true + end + end + update_active_sliders() + errorbox.visible = not count + end + -- Attach to focus change + client.connect_signal("focus",update_slider_list) + -- Update root.keys(gears.table.join( root.keys(), ask.k(":client.volume_up", function() - volume("+5") + volume(client.focus,"+5") end,{description = "increase client volume", group = "client"}), ask.k(":client.volume_down", function() - volume("-5") + volume(client.focus,"-5") end,{description = "decrease client volume", group = "client"}), ask.k(":client.volume_mute", function() - volume(0) + volume(client.focus,0) end,{description = "mute client", group = "client"}) )) return widget