diff options
Diffstat (limited to '.config/awesome/ui/statusbar/panel/widgets/mpris.lua')
-rw-r--r-- | .config/awesome/ui/statusbar/panel/widgets/mpris.lua | 429 |
1 files changed, 429 insertions, 0 deletions
diff --git a/.config/awesome/ui/statusbar/panel/widgets/mpris.lua b/.config/awesome/ui/statusbar/panel/widgets/mpris.lua new file mode 100644 index 0000000..c27bf66 --- /dev/null +++ b/.config/awesome/ui/statusbar/panel/widgets/mpris.lua @@ -0,0 +1,429 @@ +local custom = require "assets.custom" +local gdebug = require "gears.debug" +local phosphor = require "assets.phosphor" +local playerctl = require "services.playerctl" +local qcolor = require "quarrel.color" +local qpersistent = require "quarrel.persistent" +local qui = require "quarrel.ui" +local qvars = require "quarrel.vars" +local simpleicons = require "assets.simpleicons" +local wibox = require "wibox" + +local M = {} + +local client_icons = { -- this is used to map the mpris clients to icons + firefox = simpleicons.librewolf, -- librewolf uses the same player id as firefox + spotify = simpleicons.spotify, + mpd = custom.vinyl_record_fill, + __generic = phosphor.waveform_fill, -- used for any client not in the map +} + +local DEFAULTS = { + ---@type string + title = "Nothing's playing", + artist = {}, + ---@type string + album = "", + progresstext = { + ---@type string + position = "~", + ---@type string + length = "~", + }, + ---@type number + position = 0, + ---@type number + length = math.huge, + client_icon = client_icons.__generic, + art = phosphor.music_notes_fill, +} + +local bg_icon = qui.icon { + icon = DEFAULTS.client_icon, + widget = { + forced_width = 0, + forced_height = 0, + }, + color = qcolor.palette.bg.highest, +} + +local function to_hms(time) + local format = "%i:%02i" + + local h = math.floor(time / 60 ^ 2) + local m = math.floor((time / 60) % 60) + local s = math.floor(time % 60) + + if h > 0 then + format = "%i:" .. format + return string.format(format, h, m, s) + end + + return string.format(format, m, s) +end + +local function is_empty(str) + return (str == nil or #str == 0) +end + +--- mirror of playerctl:list() +--- why bother mirroring? simple: we need to track changes to the player list +--- but vanished and appeared simply don't give us an old version to compare with +---@type Playerctl.data[] +local players = {} + +---@type number +M.active_player_index = qpersistent.get "active_player_index" --[[@as number]] +if not M.active_player_index then + M.active_player_index = 1 + gdebug.print_error "failed to get active_player_index from qpersistent, falling back..." + qpersistent.store("active_player_index", M.active_player_index) +end + +--- the reason we do this instead of some other hack is this is the only way to draw the icons *without* resizing the mpris container +--- this happens because in the panel, height is unlimited, and imagebox grows until a hard limit +---@class wibox.widget.base +local client_background = wibox.container.background() + +function client_background:before_draw_children(_, cr, width, height) + cr:save() + cr:translate(width - (height / 1.25), -(height * 0.125)) + ---@diagnostic disable-next-line: missing-parameter + wibox.widget.draw_to_cairo_context(bg_icon, cr, height * 1.25, height * 1.25) + cr:restore() +end + +M.widget = wibox.widget(qui.styled { + widget = wibox.container.background, + bg = qcolor.palette.bg.high, + { + widget = client_background, + { + widget = wibox.container.margin, + margins = qui.BIG_PADDING, + { + nil, + { + { + { + widget = wibox.container.background, + bg = qcolor.palette.bg(), + shape = qui.shape, + { + widget = wibox.widget.imagebox, + image = DEFAULTS.art, + forced_height = qui.CHAR_HEIGHT * 5, + forced_width = qui.CHAR_HEIGHT * 5, + valign = "center", + halign = "center", + stylesheet = qui.recolor(qcolor.palette.bg.highest), + id = "cover", + }, + }, + { + widget = wibox.container.margin, + right = qui.BIG_PADDING, + left = qui.BIG_PADDING, + { + { + widget = wibox.container.constraint, + height = qui.CHAR_HEIGHT * 2.5, + strategy = "max", + { + widget = wibox.widget.textbox, + text = DEFAULTS.title, -- Song + id = "song", + valign = "top", + }, + }, + { + widget = wibox.container.constraint, + height = qui.CHAR_HEIGHT * 2.5, + strategy = "max", + { + widget = wibox.container.background, + fg = qcolor.palette.fg.low, + { + widget = wibox.widget.textbox, + text = DEFAULTS.artist_album, -- Artist - Album Name + id = "artist_album", + valign = "top", + }, + }, + }, + layout = wibox.layout.fixed.vertical, + }, + }, + layout = wibox.layout.fixed.horizontal, + }, + nil, + { + widget = wibox.container.margin, + top = qui.BIG_PADDING, + right = qui.BIG_PADDING, + { + { + widget = wibox.widget.textbox, + text = DEFAULTS.progresstext.position .. " / " .. DEFAULTS.progresstext.length, -- position / length + id = "progresstext", + }, + { + widget = wibox.container.place, + { + widget = wibox.widget.progressbar, + forced_height = qui.PADDING, + color = qcolor.palette.yellow(), + value = DEFAULTS.position, + max_value = DEFAULTS.length, + background_color = qcolor.palette.bg.lowest, + bar_shape = qui.shape, + shape = qui.shape, + id = "progressbar", + }, + }, + layout = wibox.layout.fixed.horizontal, + spacing = qui.BIG_PADDING, + }, + }, + layout = wibox.layout.align.vertical, + }, + { + layout = wibox.layout.flex.vertical, + spacing = qui.BIG_PADDING, + id = "client_list", + }, + layout = wibox.layout.align.horizontal, + }, + }, + }, +}) + +local layout = M.widget:get_children_by_id("client_list")[1] --[[@as wibox.layout.flex]] +local progressbar = M.widget:get_children_by_id("progressbar")[1] --[[@as wibox.widget.progressbar]] +local progresstext = M.widget:get_children_by_id("progresstext")[1] --[[@as wibox.widget.textbox]] +local song = M.widget:get_children_by_id("song")[1] --[[@as wibox.widget.textbox]] +local artist_album = M.widget:get_children_by_id("artist_album")[1] --[[@as wibox.widget.textbox]] +local cover = M.widget:get_children_by_id("cover")[1] --[[@as wibox.widget.imagebox]] + +local function mirror_player_list() + players = playerctl:list() +end + +mirror_player_list() + +local function handle_metadata(_, player) + local title, album, artist, art + if player ~= playerctl:list()[M.active_player_index] then + return + end + if player then + if player.metadata.title then + title = player.metadata.title + else + title = DEFAULTS.title + end + + if player.metadata.album then + album = player.metadata.album + else + album = DEFAULTS.album + end + + if player.metadata.artist then + artist = player.metadata.artist + else + artist = DEFAULTS.artist + end + + if player.metadata.art and player.metadata.art:match "^file://" then + art = player.metadata.art:gsub("^file://", "") + else + art = DEFAULTS.art + end + else + title = DEFAULTS.title + album = DEFAULTS.album + artist = DEFAULTS.artist + art = DEFAULTS.art + end + + artist_album.text = table.concat(artist, ", ") .. ((is_empty(artist) or is_empty(album)) and "" or " - ") .. album + song.text = title + ---@diagnostic disable-next-line:inject-field + cover.image = art +end + +local function handle_position(_, player) + if player ~= playerctl:list()[M.active_player_index] then + return + end + local position, length + local content = "" + + if player then + if player.position then + position = player.position / playerctl.unit + content = content .. to_hms(position) + else + position = DEFAULTS.position + content = content .. DEFAULTS.progresstext.position + end + + content = content .. " / " + + if player.metadata.length then + length = player.metadata.length / playerctl.unit + content = content .. to_hms(length) + else + length = DEFAULTS.length + content = content .. DEFAULTS.progresstext.length + end + else + position = DEFAULTS.position + length = DEFAULTS.length + content = DEFAULTS.progresstext.position .. " / " .. DEFAULTS.progresstext.length + end + + progresstext.text = content + ---@diagnostic disable-next-line:inject-field + progressbar.value = position + ---@diagnostic disable-next-line:inject-field + progressbar.max_value = length +end + +playerctl:connect_signal("player::metadata", handle_metadata) +playerctl:connect_signal("player::position", handle_position) + +local function update_player(player) + handle_metadata(nil, player) + handle_position(nil, player) + qpersistent.store("active_player_index", M.active_player_index) + + local client_icon = client_icons[(player or { name = "__generic" }).name] + bg_icon.image = client_icon or client_icons.__generic + client_background:emit_signal "widget::redraw_needed" + + for i, child in ipairs(layout.children) do + ---@diagnostic disable-next-line:undefined-field + child:index_handler(i) + end +end + +function M.next_player() + local players_length = #layout.children + if players_length == 0 then + return + end + + if M.active_player_index + 1 > players_length then + M.active_player_index = 1 + else + M.active_player_index = M.active_player_index + 1 + end + + update_player(playerctl:list()[M.active_player_index]) +end + +function M.previous_player() + local players_length = #layout.children + if players_length == 0 then + return + end + + if M.active_player_index - 1 < 1 then + M.active_player_index = players_length + else + M.active_player_index = M.active_player_index - 1 + end + + update_player(playerctl:list()[M.active_player_index]) +end + +---@param diff_player Playerctl.data +local function recalculate_active_player(diff_player, vanished) + if type(diff_player) ~= "table" then + return + end + if #layout.children == 0 then + M.active_player_index = 1 + update_player() + return + end + + local active_player = players[M.active_player_index] + if not active_player then -- we're recovering from a state with no players + update_player(diff_player) + return + end + + if diff_player.instance == active_player.instance and vanished then -- active player vanished; fall back to previous player + M.previous_player() + else -- non-active player appeared/vanished; try to find active player + for i, p in ipairs(playerctl:list()) do + if p.instance == active_player.instance then + M.active_player_index = i + update_player(p) + return + end + end + + gdebug.print_warning( + "failed to find active player:\n " .. gdebug.dump_return(active_player, nil, 2):gsub("\n", "\n ") + ) + M.active_player_index = 1 + update_player(playerctl:list()[M.active_player_index]) + end +end + +local function register_player(player) + local widget = wibox.widget { + widget = wibox.container.constraint, + width = qui.PADDING, + strategy = "min", + { + widget = wibox.container.background, + shape = qui.shape, + bg = qcolor.palette.bg.lowest, + }, + index_handler = function(self, index) + if M.active_player_index == index then + self.widget.bg = qcolor.palette.yellow() + else + self.widget.bg = qcolor.palette.bg.lowest + end + end, + } + + ---@diagnostic disable-next-line:undefined-field + layout:add(widget) + + recalculate_active_player(player, false) + mirror_player_list() +end + +local function unregister_player(player) + ---@diagnostic disable-next-line:undefined-field + layout:remove(#layout.children) + recalculate_active_player(player, true) + mirror_player_list() +end + +for _, player in ipairs(playerctl:list()) do + register_player(player) +end + +-- recover state +---@diagnostic disable-next-line:undefined-field +local last_active_player = playerctl:list()[M.active_player_index] + +update_player(last_active_player) + +playerctl:connect_signal("player::appeared", function(_, player) + register_player(player) +end) + +playerctl:connect_signal("player::vanished", function(_, player) + unregister_player(player) +end) + +return M |