aboutsummaryrefslogtreecommitdiff
path: root/.config/awesome/ui/statusbar/panel/widgets/mpris.lua
diff options
context:
space:
mode:
Diffstat (limited to '.config/awesome/ui/statusbar/panel/widgets/mpris.lua')
-rw-r--r--.config/awesome/ui/statusbar/panel/widgets/mpris.lua429
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