------------------------------------------------- -- mpris based Arc Widget for Awesome Window Manager -- Modelled after Pavel Makhov's work -- @author Mohammed Gaber -- requires - playerctl -- @copyright 2020 ------------------------------------------------- local awful = require('awful') local beautiful = require('beautiful') local wibox = require('wibox') local gears = require('gears') local playerctl = { player_name = nil, } function playerctl:set_player(name) self.player_name = name if self.timer ~= nil then self.timer:stop() playerctl:watch(self.watch_params.timeout, self.watch_params.callback, self.watch_params.widget) end end function playerctl:cmd(cmd) return "playerctl -p '" .. self.player_name .. "' " .. cmd end local watch_fields = { [1] = 'status', [2] = 'xesam:artist', [3] = 'xesam:title', [4] = 'mpris:artUrl', [5] = 'position', [6] = 'mpris:length', [7] = 'album', [8] = 'xesam:contentCreated', } local watch_cmd = string.format("-f '{{%s}}' metadata", table.concat(watch_fields, '}};{{')) function playerctl:watch(timeout, callback, widget) local cmd = self:cmd(watch_cmd) self.watch_params = { timeout = timeout, callback = callback, widget = widget } local cb = function(cb_widget, stdout, _, _, _) local words = gears.string.split(stdout, ';') local position, length, progress = tonumber(words[5]), tonumber(words[6]) if position ~= nil and length ~= nil and length > 0 then progress = position / length end local metadata = { status = words[1], artist = words[2], current_song = words[3], art_url = words[4], position = position, length = length, album = words[7], progress = progress, } if words[8] ~= nil then metadata.year = string.sub(words[8], 0, 4) end callback(cb_widget, metadata) end local _, timer = awful.widget.watch(cmd, timeout, cb, widget) self.timer = timer end function playerctl:toggle() awful.spawn(self:cmd('play-pause'), false) end function playerctl:next() awful.spawn(self:cmd('next'), false) end function playerctl:prev() awful.spawn(self:cmd('previous'), false) end local player_selector_popup = { popup = awful.popup { bg = beautiful.bg_normal, fg = beautiful.fg_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 = {}, }, rows = { layout = wibox.layout.fixed.vertical }, } function player_selector_popup:add_radio_button(player_name) local checkbox = wibox.widget { layout = wibox.container.place, valign = 'center', { checked = player_name == playerctl.player_name, color = beautiful.bg_normal, paddings = 2, shape = gears.shape.circle, forced_width = 20, forced_height = 20, check_color = beautiful.fg_normal, widget = wibox.widget.checkbox, }, } checkbox:connect_signal('button::press', function() playerctl:set_player(player_name) self:toggle() end) local row = wibox.widget { { { checkbox, { { text = player_name, 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, fg = beautiful.fg_normal, widget = wibox.container.background, } table.insert(self.rows, row) end function player_selector_popup:rebuild() awful.spawn.easy_async('playerctl -l', function(stdout, _, _, _) for i = 0, #self.rows do self.rows[i] = nil end for name in stdout:gmatch('[^\r\n]+') do if name ~= '' and name ~= nil then self:add_radio_button(name) end end self.popup:setup(self.rows) self.popup.visible = true self.popup:move_next_to(mouse.current_widget_geometry) end) end function player_selector_popup:toggle() if self.popup.visible then self.popup.visible = false else self:rebuild() end end local function duration(microseconds) if microseconds == nil then return '--:--' end local seconds = math.floor(microseconds / 1000000) local minutes = math.floor(seconds / 60) seconds = seconds - minutes * 60 local hours = math.floor(minutes / 60) minutes = minutes - hours * 60 if hours >= 1 then return string.format('%d:%02d:%02d', hours, minutes, seconds) end return string.format('%d:%02d', minutes, seconds) end local mpris_widget = {} local function worker(user_args) local args = user_args or {} local font = args.font or 'Roboto Condensed 16px' local path_to_icons = '/usr/share/icons/Adwaita' local pause_icon = args.pause_icon or path_to_icons .. '/symbolic/actions/media-playback-pause-symbolic.svg' local play_icon = args.play_icon or path_to_icons .. '/symbolic/actions/media-playback-start-symbolic.svg' local stop_icon = args.stop_icon or path_to_icons .. '/symbolic/actions/media-playback-stop-symbolic.svg' local library_icon = args.library_icon or path_to_icons .. '/symbolic/places/folder-music-symbolic.svg' local popup_width = args.popup_width or 300 playerctl.player_name = args.default_player or 'mpv' local icon = wibox.widget { widget = wibox.widget.imagebox, image = play_icon, } local progress_widget = wibox.widget { widget = wibox.container.arcchart, icon, min_value = 0, max_value = 1, value = 0, thickness = 2, start_angle = 4.71238898, -- 2pi*3/4 forced_height = 24, forced_width = 24, rounded_edge = true, colors = { '#ffffff11', 'black' }, paddings = 2, } local artist_widget = wibox.widget { font = font, widget = wibox.widget.textbox, } local title_widget = wibox.widget { font = font, widget = wibox.widget.textbox, } mpris_widget = wibox.widget { artist_widget, progress_widget, title_widget, spacing = 4, layout = wibox.layout.fixed.horizontal, } local cover_art_widget = wibox.widget { widget = wibox.widget.imagebox, forced_height = 0, forced_width = popup_width, resize_allowed = true, } local metadata_widget = wibox.widget { widget = wibox.widget.textbox, font = font, forced_height = 100, forced_width = popup_width, } local update_metadata = function(meta) artist_widget:set_text(meta.artist) title_widget:set_text(meta.current_song) local s = meta.album if meta.year ~= nil and #meta.year == 4 then s = s .. ' (' .. meta.year .. ')' end s = s .. '\n' .. meta.current_song .. ' (' .. duration(meta.position) .. '/' .. duration(meta.length) .. ')' metadata_widget:set_text(s) progress_widget.values = { 1.0 - (meta.progress or 0.0), meta.progress or 0.0 } -- poor man's urldecode local art_url = meta.art_url:gsub('file://', '/') art_url = art_url:gsub('%%(%x%x)', function(x) return string.char(tonumber(x, 16)) end) if art_url ~= nil and art_url ~= '' then cover_art_widget.image = art_url cover_art_widget.forced_height = popup_width else cover_art_widget.image = nil cover_art_widget.forced_height = 0 end end local update_graphic = function(widget, metadata) if metadata.current_song ~= nil then if string.len(metadata.current_song) > 40 then metadata.current_song = string.sub(metadata.current_song, 0, 38) .. '…' end end if metadata.status == 'Playing' then icon.image = pause_icon widget.colors = { beautiful.widget_main_color } update_metadata(metadata) elseif metadata.status == 'Paused' then icon.image = play_icon widget.colors = { beautiful.widget_main_color } update_metadata(metadata) elseif metadata.status == 'Stopped' then icon.image = stop_icon else -- no player is running icon.image = library_icon widget.colors = { beautiful.widget_red } end end mpris_widget:buttons( awful.util.table.join( awful.button({}, 3, function() player_selector_popup:toggle() end), awful.button({}, 4, function() playerctl:next() end), awful.button({}, 5, function() playerctl:prev() end), awful.button({}, 1, function() playerctl:toggle() end) ) ) playerctl:watch(1, update_graphic, mpris_widget) local mpris_popup = awful.popup { border_color = beautiful.border_color, ontop = true, visible = false, widget = wibox.widget { cover_art_widget, metadata_widget, layout = wibox.layout.fixed.vertical, }, } mpris_widget:connect_signal('mouse::enter', function() mpris_popup.visible = true mpris_popup:move_next_to(mouse.current_widget_geometry) end) mpris_widget:connect_signal('mouse::leave', function() mpris_popup.visible = false end) --}} return mpris_widget end return setmetatable(mpris_widget, { __call = function(_, ...) return worker(...) end, })