aboutsummaryrefslogtreecommitdiff
path: root/.config/awesome/services
diff options
context:
space:
mode:
Diffstat (limited to '.config/awesome/services')
-rw-r--r--.config/awesome/services/backlight.lua6
-rw-r--r--.config/awesome/services/battery.lua8
-rw-r--r--.config/awesome/services/common.lua60
-rw-r--r--.config/awesome/services/mpris/init.lua100
-rw-r--r--.config/awesome/services/playerctl.lua612
5 files changed, 716 insertions, 70 deletions
diff --git a/.config/awesome/services/backlight.lua b/.config/awesome/services/backlight.lua
index 02ed808..bb9bedc 100644
--- a/.config/awesome/services/backlight.lua
+++ b/.config/awesome/services/backlight.lua
@@ -1,10 +1,8 @@
local backlight = require("lib.lit").backlight.new("amdgpu_bl1", 5)
local gobject = require "gears.object"
-local gtimer = require "gears.timer"
-local naughty = require "naughty"
local phosphor = require "assets.phosphor"
+local qcolor = require "quarrel.color"
local qmath = require "quarrel.math"
-local qvars = require "quarrel.vars"
local backlight_wrapper = gobject {
class = {
@@ -27,7 +25,7 @@ backlight:connect_signal("brightness", function(_, brightness)
{ 255 },
})
- backlight_wrapper:emit_signal("icon", phosphor[icon_data .. "_fill"], qvars.colors.fg)
+ backlight_wrapper:emit_signal("icon", phosphor[icon_data .. "_fill"], qcolor.palette.fg())
backlight_wrapper:emit_signal("value", brightness)
end)
diff --git a/.config/awesome/services/battery.lua b/.config/awesome/services/battery.lua
index 98bacc9..d10714e 100644
--- a/.config/awesome/services/battery.lua
+++ b/.config/awesome/services/battery.lua
@@ -2,8 +2,8 @@ local UPower = require("lgi").UPowerGlib
local gobject = require "gears.object"
local gtimer = require "gears.timer"
local phosphor = require "assets.phosphor"
+local qcolor = require "quarrel.color"
local qmath = require "quarrel.math"
-local qvars = require "quarrel.vars"
local upower = assert(UPower.Client.new_full())
local inner
@@ -51,7 +51,11 @@ local function icon_handler()
{ 100 },
})
- battery:emit_signal("icon", phosphor["battery_vertical_" .. icon_data[1] .. "_fill"], qvars.colors[icon_data[2]])
+ battery:emit_signal(
+ "icon",
+ phosphor["battery_vertical_" .. icon_data[1] .. "_fill"],
+ qcolor.palette[icon_data[2]]()
+ )
end
local function level_handler()
diff --git a/.config/awesome/services/common.lua b/.config/awesome/services/common.lua
index ad1f998..9657c8a 100644
--- a/.config/awesome/services/common.lua
+++ b/.config/awesome/services/common.lua
@@ -1,12 +1,10 @@
local gfs = require "gears.filesystem"
local gtimer = require "gears.timer"
local phosphor = require "assets.phosphor"
+local qcolor = require "quarrel.color"
local qjson = require "quarrel.json"
local qmath = require "quarrel.math"
local qnative = require "quarrel.native"
-local qstore = require "quarrel.store"
-local qvars = require "quarrel.vars"
-local UPower = require("lgi").UPowerGlib
--- Register a service
---@param name string
@@ -39,9 +37,9 @@ if not gfs.file_readable "/tmp/wp_audio_status" then
assert(io.open("/tmp/wp_audio_status", "w")):write("{}"):close()
end
-qstore.audio_file = qnative.util.open_file "/tmp/wp_audio_status"
-qstore.brightness_file = qnative.util.open_file "/sys/class/backlight/amdgpu_bl1/actual_brightness"
-qstore.wifi_file = qnative.util.open_file "/proc/net/wireless"
+local audio_file = qnative.util.open_file "/tmp/wp_audio_status"
+-- qstore.brightness_file = qnative.util.open_file "/sys/class/backlight/amdgpu_bl1/actual_brightness"
+local wifi_file = qnative.util.open_file "/proc/net/wireless"
-- follows the format `service = { provider, icon_provider }`
---@class Service
@@ -53,7 +51,7 @@ local services = {
audio = {
-- volume, muted
function()
- local audio_status = qjson.decode(read(qstore.audio_file))
+ local audio_status = qjson.decode(read(audio_file))
local default_sink = audio_status["G435 Wireless Gaming Headset Analog Stereo"]
if not default_sink then
@@ -64,7 +62,7 @@ local services = {
end,
function(volume, muted)
if muted or not volume then
- return phosphor.speaker_simple_x_fill, qvars.colors.red
+ return phosphor.speaker_simple_x_fill, qcolor.palette.red()
end
local icon_data = qmath.step_value(volume, {
@@ -75,32 +73,32 @@ local services = {
{ 100 },
})
- return phosphor["speaker_simple_" .. icon_data .. "_fill"], qvars.colors.fg
- end,
- },
- brightness = {
- -- brightness
- function()
- return read(qstore.brightness_file, "n")
- end,
- function(brightness)
- local icon_data = qmath.step_value(brightness, {
- { 0, "cloud_moon" },
- { 51, "moon" },
- { 102, "sun_horizon" },
- { 153, "sun_dim" },
- { 204, "sun" },
- { 255 },
- })
-
- return phosphor[icon_data .. "_fill"], qvars.colors.fg
+ return phosphor["speaker_simple_" .. icon_data .. "_fill"], qcolor.palette.fg()
end,
},
+ -- brightness = {
+ -- -- brightness
+ -- function()
+ -- return read(qstore.brightness_file, "n")
+ -- end,
+ -- function(brightness)
+ -- local icon_data = qmath.step_value(brightness, {
+ -- { 0, "cloud_moon" },
+ -- { 51, "moon" },
+ -- { 102, "sun_horizon" },
+ -- { 153, "sun_dim" },
+ -- { 204, "sun" },
+ -- { 255 },
+ -- })
+ --
+ -- return phosphor[icon_data .. "_fill"], qcolor.palette.fg()
+ -- end,
+ -- },
wifi = {
-- essid, strength, connected
function()
- local lines = qstore.wifi_file:lines()
- qstore.wifi_file:rewind()
+ local lines = wifi_file:lines()
+ wifi_file:rewind()
if not lines[3] then
return nil, 0, false
@@ -112,7 +110,7 @@ local services = {
end,
function(_, strength, connected)
if not connected then
- return phosphor.wifi_x_fill, qvars.colors.red
+ return phosphor.wifi_x_fill, qcolor.palette.red()
end
local icon_data = qmath.step_value(strength, {
@@ -123,7 +121,7 @@ local services = {
{ 100 },
})
- return phosphor["wifi_" .. icon_data[1] .. "_fill"], qvars.colors[icon_data[2]]
+ return phosphor["wifi_" .. icon_data[1] .. "_fill"], qcolor.palette[icon_data[2]]()
end,
},
}
diff --git a/.config/awesome/services/mpris/init.lua b/.config/awesome/services/mpris/init.lua
new file mode 100644
index 0000000..3659a3a
--- /dev/null
+++ b/.config/awesome/services/mpris/init.lua
@@ -0,0 +1,100 @@
+local gobject = require "gears.object"
+local playerctl = require "services.mpris.playerctl"
+local qpersistent = require "quarrel.persistent"
+
+---@class ServiceMpris : gears.object
+---@field inner Playerctl
+---@field index number the index of the currently active player
+local M = {}
+
+---@param self ServiceMpris
+---@param player Playerctl.data
+local function update_player(self, player)
+ self:emit_signal("player::metadata", player)
+ self:emit_signal("player::position", player)
+ -- handle_position(nil, player)
+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
+
+function M:next_player()
+ local players = self.inner:list()
+
+ if #players == 0 then
+ return
+ elseif self.index + 1 > #players then
+ self.index = 1
+ else
+ self.index = self.index + 1
+ end
+
+ -- update_player(playerctl:list()[M.active_player_index])
+ local player = players[self.index]
+ self:emit_signal("player::metadata", player)
+ self:emit_signal("player::position", player)
+end
+
+function M:previous_player()
+ local players = self.inner:list()
+ if #players == 0 then
+ return
+ elseif self.index - 1 < #players then
+ self.index = #players
+ else
+ self.index = self.index - 1
+ end
+
+ local player = players[self.index]
+ self:emit_signal("player::metadata", player)
+ self:emit_signal("player::position", player)
+end
+
+M.inner = playerctl.new {
+ players = {},
+ metadata = {
+ album = "xesam:album",
+ title = "xesam:title",
+ artist = "xesam:artist",
+ art = "mpris:artUrl",
+ },
+}
+
+local instance = gobject { class = M }
+instance:connect_signal("property::index", function(self, index)
+ qpersistent.store("active_player_index", self.index)
+end)
+return instance
diff --git a/.config/awesome/services/playerctl.lua b/.config/awesome/services/playerctl.lua
index f6ee71a..c4be74b 100644
--- a/.config/awesome/services/playerctl.lua
+++ b/.config/awesome/services/playerctl.lua
@@ -1,45 +1,591 @@
-local playerctl = require("lib.bling.signal.playerctl").lib {
- player = { "spotify", "%any" },
+-- CREDIT: https://github.com/kosorin/awesome-rice/blob/b6813bd1bbb3fdd697a1b994784b72daaeaf405d/services/media/playerctl.lua
+
+-- DEPENDENCIES: playerctl
+
+local type = type
+local pairs = pairs
+local ipairs = ipairs
+local gobject = require "gears.object"
+local gtable = require "gears.table"
+local gtimer = require "gears.timer"
+local lgi_playerctl = require("lgi").Playerctl -- /usr/share/gtk-doc/html/playerctl/index.html
+
+---@alias Playerctl.selector
+---|>nil # Select primary player
+---| string # Select player by `instance`
+---| "%all" # Select all players
+
+---@class Playerctl.position_data
+---@field position? integer
+---@field length? integer
+
+---@class Playerctl.data
+---@field name string
+---@field instance string
+---@field playback_status lgi.Playerctl.PlaybackStatus
+---@field position integer
+---@field shuffle boolean
+---@field loop_status lgi.Playerctl.LoopStatus
+---@field volume number
+---@field metadata table<string, any>
+---@field package _position_timer? gears.timer
+
+local playerctl = {
+ lowest_priority = math.maxinteger,
+ any = { name = "%any" },
+ all = { name = "%all" },
}
-playerctl:connect_signal("metadata", function(_, ...)
- awesome.emit_signal("services::playerctl::metadata", ...)
-end)
+---@class Playerctl : gears.object
+---@field unit integer # Number of microseconds in a second.
+---@field package primary_player_data? Playerctl.data
+---@field package player_data table<string, Playerctl.data>
+---@field package tracked_metadata table<string, string>
+---@field package excluded_players table<string, boolean>
+---@field package player_priorities table<string, integer>
+---@field package manager lgi.Playerctl.PlayerManager
+playerctl.object = { unit = 1000000 }
+
+---@param player_data? Playerctl.data
+---@return Playerctl.position_data|nil
+function playerctl.object:get_position_data(player_data)
+ player_data = player_data or self.primary_player_data
+ return (player_data and player_data.metadata)
+ and {
+ position = player_data.position,
+ length = player_data.metadata.length,
+ }
+end
+
+---@param self Playerctl
+---@param instance string
+---@return lgi.Playerctl.Player|nil
+local function find_player_by_instance(self, instance)
+ for _, player in ipairs(self.manager.players) do
+ if player.player_instance == instance then
+ return player
+ end
+ end
+end
+
+---@param self Playerctl
+---@param player_selector Playerctl.selector
+---@param action fun(player: lgi.Playerctl.Player)
+local function for_each_player(self, player_selector, action)
+ local players
+ if not player_selector then
+ local player_data = self.primary_player_data
+ if player_data then
+ players = { find_player_by_instance(self, player_data.instance) }
+ end
+ elseif player_selector == playerctl.all.name then
+ players = self.manager.players
+ elseif type(player_selector) == "string" then
+ players = { find_player_by_instance(self, player_selector) }
+ end
+
+ if players then
+ for _, p in ipairs(players) do
+ action(p)
+ end
+ end
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:play_pause(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:play_pause()
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:play(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:play()
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:pause(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:pause()
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:stop(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:stop()
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:previous(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:previous()
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:next(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:next()
+ end)
+end
+
+---@param offset integer
+---@param player_selector Playerctl.selector
+function playerctl.object:skip(offset, player_selector)
+ if offset > 0 then
+ self:next(player_selector)
+ else
+ self:previous(player_selector)
+ end
+end
+
+---@param offset integer
+---@param player_selector Playerctl.selector
+function playerctl.object:rewind(offset, player_selector)
+ self:seek(-offset, player_selector)
+end
+
+---@param offset integer
+---@param player_selector Playerctl.selector
+function playerctl.object:fast_forward(offset, player_selector)
+ self:seek(offset, player_selector)
+end
+
+---@param offset integer
+---@param player_selector Playerctl.selector
+function playerctl.object:seek(offset, player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:seek(offset)
+ end)
+end
+
+---@param loop_status lgi.Playerctl.LoopStatus
+---@param player_selector Playerctl.selector
+function playerctl.object:set_loop_status(loop_status, player_selector)
+ loop_status = loop_status:upper()
+ for_each_player(self, player_selector, function(p)
+ p:set_loop_status(loop_status)
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:cycle_loop_status(player_selector)
+ for_each_player(self, player_selector, function(p)
+ if p.loop_status == "NONE" then
+ p:set_loop_status "TRACK"
+ elseif p.loop_status == "TRACK" then
+ p:set_loop_status "PLAYLIST"
+ elseif p.loop_status == "PLAYLIST" then
+ p:set_loop_status "NONE"
+ end
+ end)
+end
+
+---@param position integer
+---@param player_selector Playerctl.selector
+function playerctl.object:set_position(position, player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:set_position(position)
+ end)
+end
+
+---@param shuffle boolean
+---@param player_selector Playerctl.selector
+function playerctl.object:set_shuffle(shuffle, player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:set_shuffle(shuffle)
+ end)
+end
+
+---@param player_selector Playerctl.selector
+function playerctl.object:toggle_shuffle(player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:set_shuffle(not p.shuffle)
+ end)
+end
+
+---@param volume number
+---@param player_selector Playerctl.selector
+function playerctl.object:set_volume(volume, player_selector)
+ for_each_player(self, player_selector, function(p)
+ p:set_volume(volume)
+ end)
+end
+
+---@param player_data? Playerctl.data
+---@return boolean
+function playerctl.object:is_primary_player(player_data)
+ return self.primary_player_data == player_data
+end
+
+---@return Playerctl.data|nil
+function playerctl.object:get_primary_player_data()
+ return self.primary_player_data
+end
+
+---@param self Playerctl
+---@param player? lgi.Playerctl.Player
+local function update_primary_player(self, player)
+ if player then
+ self.manager:move_player_to_top(player)
+ end
+
+ local primary_player = self.manager.players[1]
+
+ local old = self.primary_player_data
+ local new = self.player_data[primary_player and primary_player.player_instance]
+ if old ~= new then
+ self.primary_player_data = new
+ self:emit_signal("player::primary", new, old)
+ end
+end
+
+---@param player_data Playerctl.data
+---@return boolean
+function playerctl.object:is_pinned(player_data)
+ local player_instance = player_data and player_data.instance or nil
+ return self.pinned_player_instance == player_instance
+end
+
+---@param player_data? Playerctl.data
+function playerctl.object:pin(player_data)
+ local player_instance = player_data and player_data.instance or nil
+ if self.pinned_player_instance ~= player_instance then
+ self.pinned_player_instance = player_instance
+ self:emit_signal("player::pinned", self.pinned_player_instance)
+
+ local player
+ if player_data then
+ player = find_player_by_instance(self, player_data.instance)
+ elseif self.primary_player_data then
+ player = find_player_by_instance(self, self.primary_player_data.instance)
+ end
+
+ update_primary_player(self, player)
+ end
+end
+
+---@return Playerctl.data[]
+function playerctl.object:list()
+ ---@type Playerctl.data[]
+ local players = {}
+ for _, p in ipairs(self.manager.players) do
+ players[#players + 1] = self.player_data[p.player_instance]
+ end
+ return players
+end
+
+---@param player_data Playerctl.data
+local function refresh_position_timer(player_data)
+ if player_data.playback_status == "PLAYING" then
+ player_data._position_timer:again()
+ else
+ player_data._position_timer:stop()
+ end
+end
+
+---@param self Playerctl
+---@param player_data Playerctl.data
+---@param by_timer? boolean
+local function update_position(self, player_data, by_timer)
+ local player = find_player_by_instance(self, player_data.instance)
+ if player then
+ player_data.position = player:get_position()
+ self:emit_signal("player::position", player_data, by_timer)
+ end
+end
-playerctl:connect_signal("position", function(_, ...)
- awesome.emit_signal("services::playerctl::position", ...)
-end)
+---@param self Playerctl
+---@param player_data Playerctl.data
+---@param metadata lgi.Playerctl.Metadata
+---@return boolean
+---@return table<string, boolean>
+local function update_metadata(self, player_data, metadata)
+ metadata = metadata and metadata.value or {}
-playerctl:connect_signal("playback_status", function(_, ...)
- awesome.emit_signal("services::playerctl::playback_status", ...)
-end)
+ local changed = false
+ local changed_data = {}
-playerctl:connect_signal("seeked", function(_, ...)
- awesome.emit_signal("services::playerctl::seeked", ...)
-end)
+ local function mark_changed(name)
+ changed = true
+ changed_data[name] = true
+ end
-playerctl:connect_signal("volume", function(_, ...)
- awesome.emit_signal("services::playerctl::volume", ...)
-end)
+ local target_metadata = player_data.metadata
+ for name, mpris_name in pairs(self.tracked_metadata) do
+ local value = metadata[mpris_name]
+ local value_type = type(value)
+ if value_type == "nil" or value_type == "boolean" or value_type == "number" or value_type == "string" then
+ if target_metadata[name] ~= value then
+ target_metadata[name] = value
+ mark_changed(name)
+ end
+ elseif value_type == "userdata" and value.type == "as" then
+ local old = target_metadata[name]
+ if type(old) ~= "table" then
+ old = {}
+ end
-playerctl:connect_signal("loop_status", function(_, ...)
- awesome.emit_signal("services::playerctl::loop_status", ...)
-end)
+ local new = {}
+ for _, s in value:ipairs() do
+ new[#new + 1] = s
+ end
-playerctl:connect_signal("shuffle", function(_, ...)
- awesome.emit_signal("services::playerctl::shuffle", ...)
-end)
+ target_metadata[name] = new
-playerctl:connect_signal("exit", function(_, ...)
- awesome.emit_signal("services::playerctl::exit", ...)
-end)
+ if #old ~= #new then
+ mark_changed(name)
+ else
+ for i = 1, #new do
+ if old[i] ~= new[i] then
+ mark_changed(name)
+ break
+ end
+ end
+ end
+ else
+ if target_metadata[name] ~= nil then
+ target_metadata[name] = nil
+ mark_changed(name)
+ end
+ end
+ end
-playerctl:connect_signal("exit", function(_, ...)
- awesome.emit_signal("services::playerctl::exit", ...)
-end)
+ return changed, changed_data
+end
-playerctl:connect_signal("no_players", function()
- awesome.emit_signal "services::playerctl::no_players"
-end)
+---@param self Playerctl
+---@param full_name lgi.Playerctl.PlayerName
+---@return lgi.Playerctl.Player
+local function manage_player(self, full_name)
+ local new_player = lgi_playerctl.Player.new_from_name(full_name)
-return playerctl
+ function new_player.on_metadata(p, metadata)
+ local player_data = self.player_data[p.player_instance]
+ if player_data and update_metadata(self, player_data, metadata) then
+ self:emit_signal("player::metadata", player_data)
+
+ update_position(self, player_data)
+ refresh_position_timer(player_data)
+ end
+ end
+
+ function new_player.on_playback_status(p, playback_status)
+ update_primary_player(self, p)
+
+ local player_data = self.player_data[p.player_instance]
+ if player_data and player_data.playback_status ~= playback_status then
+ player_data.playback_status = playback_status
+ self:emit_signal("player::playback_status", player_data)
+
+ update_position(self, player_data)
+ refresh_position_timer(player_data)
+ end
+ end
+
+ function new_player.on_seeked(p, position)
+ local player_data = self.player_data[p.player_instance]
+ if player_data and player_data.position ~= position then
+ player_data.position = position
+ self:emit_signal("player::position", player_data)
+
+ refresh_position_timer(player_data)
+ end
+ end
+
+ function new_player.on_shuffle(p, shuffle)
+ local player_data = self.player_data[p.player_instance]
+ if player_data and player_data.shuffle ~= shuffle then
+ player_data.shuffle = shuffle
+ self:emit_signal("player::shuffle", player_data)
+ end
+ end
+
+ function new_player.on_loop_status(p, loop_status)
+ local player_data = self.player_data[p.player_instance]
+ if player_data and player_data.loop_status ~= loop_status then
+ player_data.loop_status = loop_status
+ self:emit_signal("player::loop_status", player_data)
+ end
+ end
+
+ function new_player.on_volume(p, volume)
+ local player_data = self.player_data[p.player_instance]
+ if player_data and player_data.volume ~= volume then
+ player_data.volume = volume
+ self:emit_signal("player::volume", player_data)
+ end
+ end
+
+ self.manager:manage_player(new_player)
+
+ return new_player
+end
+
+---@param self Playerctl
+---@param player_name string
+---@return boolean
+local function filter_name(self, player_name)
+ if self.excluded_players[player_name] then
+ return false
+ end
+ if self.player_priorities[playerctl.any] or self.player_priorities[player_name] then
+ return true
+ end
+ return false
+end
+
+---@param self Playerctl
+---@param player_a lgi.Playerctl.Player
+---@param player_b lgi.Playerctl.Player
+---@return sign
+local function compare_players(self, player_a, player_b)
+ if player_a.player_name < player_b.player_name then
+ return -1
+ elseif player_a.player_name > player_b.player_name then
+ return 1
+ else
+ return 0
+ end
+end
+
+---@param self Playerctl
+local function initialize_manager(self)
+ self.player_data = {}
+
+ self.manager = lgi_playerctl.PlayerManager()
+ self.manager:set_sort_func(function(a, b)
+ local player_a = lgi_playerctl.Player(a)
+ local player_b = lgi_playerctl.Player(b)
+ return compare_players(self, player_a, player_b)
+ end)
+
+ ---@param full_name lgi.Playerctl.PlayerName
+ ---@return lgi.Playerctl.Player|nil
+ local function try_manage(full_name)
+ if filter_name(self, full_name.name) then
+ return manage_player(self, full_name)
+ end
+ end
+
+ function self.manager.on_name_appeared(_, full_name)
+ try_manage(full_name)
+ end
+
+ function self.manager.on_player_appeared(_, player)
+ ---@type Playerctl.data
+ local player_data = {
+ name = player.player_name,
+ instance = player.player_instance,
+ playback_status = player.playback_status,
+ position = player.position,
+ shuffle = player.shuffle,
+ loop_status = player.loop_status,
+ volume = player.volume,
+ metadata = {},
+ }
+ update_metadata(self, player_data, player.metadata)
+
+ player_data._position_timer = gtimer {
+ timeout = 1,
+ callback = function()
+ update_position(self, player_data, true)
+ end,
+ }
+ refresh_position_timer(player_data)
+
+ self.player_data[player_data.instance] = player_data
+ self:emit_signal("player::appeared", player_data)
+
+ update_primary_player(self, player)
+ end
+
+ function self.manager.on_player_vanished(_, player)
+ update_primary_player(self)
+
+ if self.pinned_player_instance == player.player_instance then
+ self:pin(nil)
+ end
+
+ local player_data = self.player_data[player.player_instance]
+ if player_data then
+ player_data._position_timer:stop()
+ self:emit_signal("player::vanished", player_data)
+ self.player_data[player.player_instance] = nil
+ end
+ end
+
+ for _, full_name in ipairs(self.manager.player_names) do
+ try_manage(full_name)
+ end
+
+ update_primary_player(self)
+end
+
+---@param self Playerctl
+---@param args? Playerctl.new.args
+local function parse_args(self, args)
+ args = args or {}
+
+ self.tracked_metadata = args.metadata or {}
+
+ -- Always track length
+ self.tracked_metadata.length = "mpris:length"
+
+ local excluded_players = {}
+ if type(args.excluded_players) == "string" then
+ excluded_players[args.excluded_players] = true
+ elseif args.excluded_players then
+ for _, name in ipairs(args.excluded_players) do
+ excluded_players[name] = true
+ end
+ end
+ self.excluded_players = excluded_players
+
+ local function get_priority_key(name)
+ return name == playerctl.any.name and playerctl.any or name
+ end
+ local player_priorities
+ if type(args.players) == "string" then
+ player_priorities = { [get_priority_key(args.players)] = 1 }
+ elseif type(args.players) == "table" and #args.players > 0 then
+ player_priorities = {}
+ for i, name in ipairs(args.players) do
+ player_priorities[get_priority_key(name)] = i
+ end
+ else
+ player_priorities = { [playerctl.any] = 1 }
+ end
+ self.player_priorities = player_priorities
+end
+
+---@class Playerctl.new.args
+---@field players string[]
+---@field excluded_players? string[]
+---@field metadata table<string, string>
+
+---@param args? Playerctl.new.args
+---@return Playerctl
+function playerctl.new(args)
+ local self = gtable.crush(gobject {}, playerctl.object, true) --[[@as Playerctl]]
+
+ parse_args(self, args)
+
+ initialize_manager(self)
+
+ return self
+end
+
+return playerctl.new {
+ players = {},
+ metadata = {
+ album = "xesam:album",
+ title = "xesam:title",
+ artist = "xesam:artist",
+ art = "mpris:artUrl",
+ },
+}