2023-01-19 13:42:20 +00:00
-- 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 <https://www.gnu.org/licenses/>.
-- 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 " )
2023-08-30 23:17:49 +00:00
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
} )
2023-01-19 13:42:20 +00:00
local result = test_pactl
if _VERSION : match ( " 5.1 " ) then
result = ( test_pactl == 0 )
end
if not result then
2023-03-05 12:53:37 +00:00
return
2023-01-19 13:42:20 +00:00
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 " ,
2023-08-30 23:17:49 +00:00
awmtk2.generic . oneline_widget , args.style )
2023-01-19 13:42:20 +00:00
local templates = awmtk2.create_template_lib ( " client_volume " , awmtk2.templates , args.templates )
2023-08-30 23:17:49 +00:00
local t = awmtk2.build_templates ( templates , style )
2023-01-19 13:42:20 +00:00
local widget = wibox.widget ( t.container ( {
2023-08-30 23:17:49 +00:00
{
t.icon ( {
image = get_icon ( 0 ) ;
resize = true
} ) ,
2023-01-19 13:42:20 +00:00
t.textbox ( {
2023-08-30 23:17:49 +00:00
markup = " No sound/Not available "
2023-01-19 13:42:20 +00:00
} ) ,
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
} ) )
2023-08-30 23:17:49 +00:00
local client_volume_container = widget : get_children_by_id ( " client_volume_container " ) [ 1 ]
2023-01-19 13:42:20 +00:00
local errorbox = widget : get_children_by_id ( " error " ) [ 1 ]
2023-08-30 23:17:49 +00:00
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 )
2023-01-19 13:42:20 +00:00
end
end
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
end
2023-08-30 23:17:49 +00:00
end
for k , _ in pairs ( active_sliders ) do
if not checked_sliders [ k ] then
remove_slider ( k )
2023-01-19 13:42:20 +00:00
end
end
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
} )
-- Function to set client volume
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
end
2023-08-30 23:17:49 +00:00
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 ) .. " % " )
2023-01-19 13:42:20 +00:00
end
end
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
end )
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
end
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
end
2023-08-30 23:17:49 +00:00
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
2023-01-19 13:42:20 +00:00
root.keys ( gears.table . join (
root.keys ( ) ,
ask.k ( " :client.volume_up " , function ( )
2023-08-30 23:17:49 +00:00
volume ( client.focus , " +5 " )
2023-01-19 13:42:20 +00:00
end , { description = " increase client volume " , group = " client " } ) ,
ask.k ( " :client.volume_down " , function ( )
2023-08-30 23:17:49 +00:00
volume ( client.focus , " -5 " )
2023-01-19 13:42:20 +00:00
end , { description = " decrease client volume " , group = " client " } ) ,
ask.k ( " :client.volume_mute " , function ( )
2023-08-30 23:17:49 +00:00
volume ( client.focus , 0 )
2023-01-19 13:42:20 +00:00
end , { description = " mute client " , group = " client " } )
) )
return widget
end