-- This file is part of Reno desktop. -- -- Reno desktop 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. -- -- Reno desktop 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 Reno desktop. If not, see . -- Pulseaudio per-client volume setting local awful = require("awful") local gears = require("gears") local wibox = require("wibox") local awmtk2 = require("awmtk2") local fastyaml = require("parsers").fast_split_yaml 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) end if not result then return end local function get_icon(percent) if percent >= 66 then return beautiful["volume-high-symbolic"] elseif percent >= 33 then return beautiful["volume-medium-symbolic"] elseif percent > 0 then return beautiful["volume-low-symbolic"] else return beautiful["volume-muted-symbolic"] end end return function(args) local style = awmtk2.create_style("client_volume", 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) local widget = wibox.widget(t.container({ { 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) 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) local slider_icon_container = wibox.widget(t.icon({ id = "client_volume_icon", resize = true, })) 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, })) 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 slider_icon.image = get_icon(slider.value) end) 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 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 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(client.focus,"+5") end,{description = "increase client volume", group = "client"}), ask.k(":client.volume_down", function() volume(client.focus,"-5") end,{description = "decrease client volume", group = "client"}), ask.k(":client.volume_mute", function() volume(client.focus,0) end,{description = "mute client", group = "client"}) )) return widget end