|
|
@ -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({ |
|
|
|
id = "client_volume_icon", |
|
|
|
resize = true, |
|
|
|
}), |
|
|
|
(args.vertical and { |
|
|
|
{ |
|
|
|
t.icon({ |
|
|
|
image = get_icon(0); |
|
|
|
resize = true |
|
|
|
}), |
|
|
|
t.textbox({ |
|
|
|
id = "error" |
|
|
|
markup = "No sound/Not available" |
|
|
|
}), |
|
|
|
widget = wibox.container.rotate, |
|
|
|
direction = "east" |
|
|
|
}) or t.textbox({ |
|
|
|
id = "error" |
|
|
|
}), |
|
|
|
t.slider({ |
|
|
|
minimum = 0, |
|
|
|
maximum = 100, |
|
|
|
id = "client_volume", |
|
|
|
value = -1 |
|
|
|
}), |
|
|
|
layout = (args.vertical and wibox.layout.fixed.vertical) or |
|
|
|
wibox.layout.fixed.horizontal |
|
|
|
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 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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
}) |
|
|
|
-- 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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
volume_lock = false |
|
|
|
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, |
|
|
|
})) |
|
|
|
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 |
|
|
|
-- 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 |
|
|
|