mpris: add unicode support; add position tag

pull/2011/head
chayleaf 2023-02-17 03:41:38 +07:00
parent a53c97f7f6
commit 5383f7bd56
3 changed files with 176 additions and 38 deletions

View File

@ -40,6 +40,7 @@ class Mpris : public AModule {
std::optional<std::string> album; std::optional<std::string> album;
std::optional<std::string> title; std::optional<std::string> title;
std::optional<std::string> length; // as HH:MM:SS std::optional<std::string> length; // as HH:MM:SS
std::optional<std::string> position; // same format
}; };
auto getPlayerInfo() -> std::optional<PlayerInfo>; auto getPlayerInfo() -> std::optional<PlayerInfo>;
@ -48,6 +49,7 @@ class Mpris : public AModule {
auto getAlbumStr(const PlayerInfo&, bool) -> std::string; auto getAlbumStr(const PlayerInfo&, bool) -> std::string;
auto getTitleStr(const PlayerInfo&, bool) -> std::string; auto getTitleStr(const PlayerInfo&, bool) -> std::string;
auto getLengthStr(const PlayerInfo&, bool) -> std::string; auto getLengthStr(const PlayerInfo&, bool) -> std::string;
auto getPositionStr(const PlayerInfo&, bool) -> std::string;
auto getDynamicStr(const PlayerInfo&, bool, bool) -> std::string; auto getDynamicStr(const PlayerInfo&, bool, bool) -> std::string;
Gtk::Box box_; Gtk::Box box_;
@ -69,7 +71,9 @@ class Mpris : public AModule {
int title_len_; int title_len_;
int dynamic_len_; int dynamic_len_;
std::vector<std::string> dynamic_prio_; std::vector<std::string> dynamic_prio_;
bool truncate_hours_;
bool tooltip_len_limits_; bool tooltip_len_limits_;
std::string ellipsis_;
std::chrono::seconds interval_; std::chrono::seconds interval_;
std::string player_; std::string player_;

View File

@ -50,19 +50,23 @@ The *mpris* module displays currently playing media via libplayerctl.
*artist-len*: ++ *artist-len*: ++
typeof: integer ++ typeof: integer ++
Maximum length of the Artist tag. Maximum length of the Artist tag (Wide/Fullwidth Unicode characters
count as two).
*album-len*: ++ *album-len*: ++
typeof: integer ++ typeof: integer ++
Maximum length of the Album tag. Maximum length of the Album tag (Wide/Fullwidth Unicode characters count
as two).
*title-len*: ++ *title-len*: ++
typeof: integer ++ typeof: integer ++
Maximum length of the Title tag. Maximum length of the Title tag (Wide/Fullwidth Unicode characters count
as two).
*dynamic-len*: ++ *dynamic-len*: ++
typeof: integer ++ typeof: integer ++
Maximum length of the Dynamic tag. Maximum length of the Dynamic tag (Wide/Fullwidth Unicode characters
count as two).
*dynamic-priority* ++ *dynamic-priority* ++
typeof: []string ++ typeof: []string ++
@ -70,10 +74,26 @@ The *mpris* module displays currently playing media via libplayerctl.
Priority of the tags when truncating the Dynamic tag (absence in this Priority of the tags when truncating the Dynamic tag (absence in this
list means force inclusion). list means force inclusion).
*truncate-hours*: ++
typeof: bool ++
default: true ++
Whether to truncate hours when media duration is less than an hour long
*enable-tooltip-len-limits*: ++ *enable-tooltip-len-limits*: ++
typeof: bool ++ typeof: bool ++
default: false ++ default: false ++
Option to enable the limits for the tooltip as well Option to enable the length limits for the tooltip as well
*ellipsis*: ++
typeof: string ++
default: "…" ++
Override the default ellipsis (set to empty string to simply truncate
the tags when needed instead).
*on-click*: ++
typeof: string ++
default: play-pause ++
Overwrite default action toggles.
*on-click*: ++ *on-click*: ++
typeof: string ++ typeof: string ++

View File

@ -1,15 +1,16 @@
#include "modules/mpris/mpris.hpp"
#include <fmt/core.h> #include <fmt/core.h>
#include <optional> #include <optional>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "modules/mpris/mpris.hpp"
extern "C" { extern "C" {
#include <playerctl/playerctl.h> #include <playerctl/playerctl.h>
} }
#include <glib.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
namespace waybar::modules::mpris { namespace waybar::modules::mpris {
@ -26,8 +27,11 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
album_len_(-1), album_len_(-1),
title_len_(-1), title_len_(-1),
dynamic_len_(-1), dynamic_len_(-1),
dynamic_prio_({"title", "length", "artist", "album"}), dynamic_prio_({"title", "length", "position", "artist", "album"}),
truncate_hours_(true),
tooltip_len_limits_(false), tooltip_len_limits_(false),
// this character is used in Gnome so it's fine to use it here
ellipsis_(u8"\u2026"),
interval_(0), interval_(0),
player_("playerctld"), player_("playerctld"),
manager(), manager(),
@ -49,6 +53,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
if (config_["format-stopped"].isString()) { if (config_["format-stopped"].isString()) {
format_stopped_ = config_["format-stopped"].asString(); format_stopped_ = config_["format-stopped"].asString();
} }
if (config_["ellipsis"].isString()) {
ellipsis_ = config_["ellipsis"].asString();
}
if (tooltipEnabled()) { if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) { if (config_["tooltip-format"].isString()) {
tooltip_ = config_["tooltip-format"].asString(); tooltip_ = config_["tooltip-format"].asString();
@ -89,6 +96,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config)
} }
} }
if (config_["truncate-hours"].isBool()) {
truncate_hours_ = config["truncate-hours"].asBool();
}
if (config_["interval"].isUInt()) { if (config_["interval"].isUInt()) {
interval_ = std::chrono::seconds(config_["interval"].asUInt()); interval_ = std::chrono::seconds(config_["interval"].asUInt());
} }
@ -178,24 +188,86 @@ auto Mpris::getIcon(const Json::Value& icons, const std::string& key) -> std::st
return ""; return "";
} }
// Wide characters count as two, zero-width characters count as zero
// Modifies str in-place (unless width = std::string::npos)
// Returns the total width of the string pre-truncating
size_t utf8_truncate(std::string& str, size_t width = std::string::npos) {
if (str.length() == 0) return 0;
const gchar* trunc_end = nullptr;
size_t total_width = 0;
for (gchar *data = str.data(), *end = data + str.size(); data;) {
gunichar c = g_utf8_get_char_validated(data, end - data);
if (c == -1 || c == -2) {
// invalid unicode, treat string as ascii
if (width != std::string::npos && str.length() > width) str.resize(width);
return str.length();
} else if (g_unichar_iswide(c)) {
total_width += 2;
} else if (!g_unichar_iszerowidth(c)) {
total_width += 1;
}
data = g_utf8_find_next_char(data, end);
if (width != std::string::npos && total_width <= width) trunc_end = data;
}
if (trunc_end) str.resize(trunc_end - str.data());
return total_width;
}
size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast<std::string&>(str)); }
void truncate(std::string& s, const std::string& ellipsis, size_t max_len) {
if (max_len == 0) {
s.resize(0);
return;
}
size_t len = utf8_truncate(s, max_len);
if (len > max_len) {
size_t ellipsis_len = utf8_width(ellipsis);
if (max_len >= ellipsis_len) {
if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len);
s += ellipsis;
} else {
s.resize(0);
}
}
}
auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string {
std::string artist = info.artist.value_or(std::string()); std::string artist = info.artist.value_or(std::string());
return (truncated && artist_len_ >= 0) ? artist.substr(0, artist_len_) : artist; if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_);
return artist;
} }
auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string {
std::string album = info.album.value_or(std::string()); std::string album = info.album.value_or(std::string());
return (truncated && album_len_ >= 0) ? album.substr(0, album_len_) : album; if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_);
return album;
} }
auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string {
std::string title = info.title.value_or(std::string()); std::string title = info.title.value_or(std::string());
return (truncated && title_len_ >= 0) ? title.substr(0, title_len_) : title; if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_);
return title;
} }
auto Mpris::getLengthStr(const PlayerInfo &info, bool from_dynamic) -> std::string { auto Mpris::getLengthStr(const PlayerInfo& info, bool truncated) -> std::string {
if (info.length.has_value()) { if (info.length.has_value()) {
return from_dynamic ? ("[" + info.length.value() + "]") : info.length.value(); std::string length = info.length.value();
return (truncated && length.substr(0, 3) == "00:") ? length.substr(3) : length;
}
return std::string();
}
auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::string {
if (info.position.has_value()) {
std::string position = info.position.value();
return (truncated && position.substr(0, 3) == "00:") ? position.substr(3) : position;
} }
return std::string(); return std::string();
} }
@ -204,21 +276,28 @@ auto Mpris::getDynamicStr(const PlayerInfo &info, bool truncated, bool html) ->
std::string artist = getArtistStr(info, truncated); std::string artist = getArtistStr(info, truncated);
std::string album = getAlbumStr(info, truncated); std::string album = getAlbumStr(info, truncated);
std::string title = getTitleStr(info, truncated); std::string title = getTitleStr(info, truncated);
std::string length = getLengthStr(info, true); std::string length = getLengthStr(info, truncated && truncate_hours_);
// keep position format same as length format
std::string position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6);
std::stringstream dynamic; size_t artistLen = utf8_width(artist);
bool showArtist, showAlbum, showTitle, showLength; size_t albumLen = utf8_width(album);
showArtist = artist.length() != 0; size_t titleLen = utf8_width(title);
showAlbum = album.length() != 0; size_t lengthLen = length.length();
showTitle = title.length() != 0; size_t posLen = position.length();
showLength = length.length() != 0;
bool showArtist = artistLen != 0;
bool showAlbum = albumLen != 0;
bool showTitle = titleLen != 0;
bool showLength = lengthLen != 0;
bool showPos = posLen != 0;
if (truncated && dynamic_len_ >= 0) { if (truncated && dynamic_len_ >= 0) {
size_t dynamicLen = dynamic_len_; size_t dynamicLen = dynamic_len_;
size_t artistLen = showArtist ? artist.length() + 3 : 0; if (showArtist) artistLen += 3;
size_t albumLen = showAlbum ? album.length() + 3 : 0; if (showAlbum) albumLen += 3;
size_t titleLen = title.length(); if (showLength) lengthLen += 3;
size_t lengthLen = showLength ? length.length() + 1 : 0; if (showPos) posLen += 3;
size_t totalLen = 0; size_t totalLen = 0;
@ -246,23 +325,34 @@ auto Mpris::getDynamicStr(const PlayerInfo &info, bool truncated, bool html) ->
showLength = false; showLength = false;
} else { } else {
totalLen += lengthLen; totalLen += lengthLen;
posLen = std::max((size_t)2, posLen) - 2;
}
} else if (*it == "position") {
if (totalLen + posLen > dynamicLen) {
showPos = false;
} else {
totalLen += posLen;
lengthLen = std::max((size_t)2, lengthLen) - 2;
} }
} }
} }
} }
std::stringstream dynamic;
if (showArtist) dynamic << artist << " - "; if (showArtist) dynamic << artist << " - ";
if (showAlbum) dynamic << album << " - "; if (showAlbum) dynamic << album << " - ";
if (showTitle) dynamic << title; if (showTitle) dynamic << title;
if (showLength) { if (showLength || showPos) {
dynamic << " "; dynamic << " ";
if (html) { if (html) dynamic << "<small>";
dynamic << "<small>"; dynamic << '[';
} if (showPos) {
dynamic << length; dynamic << position;
if (html) { if (showLength) dynamic << '/';
dynamic << "</small>";
} }
if (showLength) dynamic << length;
dynamic << ']';
if (html) dynamic << "</small>";
} }
return dynamic.str(); return dynamic.str();
} }
@ -412,6 +502,22 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
} }
if (error) goto errorexit; if (error) goto errorexit;
{
auto position_ = playerctl_player_get_position(player, &error);
if (error) {
// it's fine to have an error here because not all players report a position
g_error_free(error);
error = nullptr;
} else {
spdlog::debug("mpris[{}]: position = {}", info.name, position_);
std::chrono::microseconds len = std::chrono::microseconds(position_);
auto len_h = std::chrono::duration_cast<std::chrono::hours>(len);
auto len_m = std::chrono::duration_cast<std::chrono::minutes>(len - len_h);
auto len_s = std::chrono::duration_cast<std::chrono::seconds>(len - len_m);
info.position = fmt::format("{:02}:{:02}:{:02}", len_h.count(), len_m.count(), len_s.count());
}
}
return info; return info;
errorexit: errorexit:
@ -508,12 +614,20 @@ auto Mpris::update() -> void {
break; break;
} }
std::string length = getLengthStr(info, truncate_hours_);
std::string tooltipLength =
(tooltip_len_limits_ || length.length() > 5) ? length : getLengthStr(info, false);
// keep position format same as length format
std::string position = getPositionStr(info, truncate_hours_ && length.length() < 6);
std::string tooltipPosition =
(tooltip_len_limits_ || position.length() > 5) ? position : getPositionStr(info, false);
try { try {
auto label_format = fmt::format( auto label_format = fmt::format(
fmt::runtime(formatstr), fmt::arg("player", info.name), fmt::runtime(formatstr), fmt::arg("player", info.name),
fmt::arg("status", info.status_string), fmt::arg("artist", getArtistStr(info, true)), fmt::arg("status", info.status_string), fmt::arg("artist", getArtistStr(info, true)),
fmt::arg("title", getTitleStr(info, true)), fmt::arg("album", getAlbumStr(info, true)), fmt::arg("title", getTitleStr(info, true)), fmt::arg("album", getAlbumStr(info, true)),
fmt::arg("length", getLengthStr(info, false)), fmt::arg("length", length), fmt::arg("position", position),
fmt::arg("dynamic", getDynamicStr(info, true, true)), fmt::arg("dynamic", getDynamicStr(info, true, true)),
fmt::arg("player_icon", getIcon(config_["player-icons"], info.name)), fmt::arg("player_icon", getIcon(config_["player-icons"], info.name)),
fmt::arg("status_icon", getIcon(config_["status-icons"], info.status_string))); fmt::arg("status_icon", getIcon(config_["status-icons"], info.status_string)));
@ -531,7 +645,7 @@ auto Mpris::update() -> void {
fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)),
fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), fmt::arg("title", getTitleStr(info, tooltip_len_limits_)),
fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)),
fmt::arg("length", getLengthStr(info, false)), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition),
fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)),
fmt::arg("player_icon", getIcon(config_["player-icons"], info.name)), fmt::arg("player_icon", getIcon(config_["player-icons"], info.name)),
fmt::arg("status_icon", getIcon(config_["status-icons"], info.status_string))); fmt::arg("status_icon", getIcon(config_["status-icons"], info.status_string)));