Merge branch 'master' into patch-1

pull/1369/head
Kogasa 2022-01-14 11:27:24 -06:00 committed by GitHub
commit e5b3bbbea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 267 additions and 96 deletions

View File

@ -2,4 +2,4 @@
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev sndio-dev scdoc libxkbcommon RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev sndio-dev scdoc libxkbcommon tzdata

View File

@ -1,21 +1,14 @@
#pragma once #pragma once
#include <fmt/format.h>
#if FMT_VERSION < 60000
#include <fmt/time.h>
#else
#include <fmt/chrono.h>
#endif
#include <date/tz.h> #include <date/tz.h>
#include "ALabel.hpp" #include "ALabel.hpp"
#include "util/sleeper_thread.hpp" #include "util/sleeper_thread.hpp"
namespace waybar::modules { namespace waybar {
struct waybar_time { struct waybar_time;
std::locale locale;
date::zoned_seconds ztime; namespace modules {
};
const std::string kCalendarPlaceholder = "calendar"; const std::string kCalendarPlaceholder = "calendar";
@ -43,4 +36,5 @@ class Clock : public ALabel {
bool is_timezone_fixed(); bool is_timezone_fixed();
}; };
} // namespace waybar::modules } // namespace modules
} // namespace waybar

View File

@ -0,0 +1,39 @@
#pragma once
#include <date/tz.h>
#include <fmt/format.h>
namespace waybar {
struct waybar_time {
std::locale locale;
date::zoned_seconds ztime;
};
} // namespace waybar
template <>
struct fmt::formatter<waybar::waybar_time> {
std::string_view specs;
template <typename ParseContext>
constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
auto it = ctx.begin();
if (it != ctx.end() && *it == ':') {
++it;
}
auto end = it;
while (end != ctx.end() && *end != '}') {
++end;
}
if (end != it) {
specs = {it, std::string_view::size_type(end - it)};
}
return end;
}
template <typename FormatContext>
auto format(const waybar::waybar_time& t, FormatContext& ctx) {
return format_to(ctx.out(), "{}", date::format(t.locale, fmt::to_string(specs), t.ztime));
}
};

View File

@ -84,12 +84,20 @@ Addressed by *memory*
*{percentage}*: Percentage of memory in use. *{percentage}*: Percentage of memory in use.
*{swapPercentage}*: Percentage of swap in use.
*{total}*: Amount of total memory available in GiB. *{total}*: Amount of total memory available in GiB.
*{swapTotal}*: Amount of total swap available in GiB.
*{used}*: Amount of used memory in GiB. *{used}*: Amount of used memory in GiB.
*{swapUsed}*: Amount of used swap in GiB.
*{avail}*: Amount of available memory in GiB. *{avail}*: Amount of available memory in GiB.
*{swapAvail}*: Amount of available swap in GiB.
# EXAMPLES # EXAMPLES
``` ```

View File

@ -69,10 +69,6 @@ Addressed by *sway/workspaces*
typeof: string ++ typeof: string ++
Command to execute when the module is updated. Command to execute when the module is updated.
*numeric-first*: ++
typeof: bool ++
Whether to put workspaces starting with numbers before workspaces that do not start with a number.
*disable-auto-back-and-forth*: ++ *disable-auto-back-and-forth*: ++
typeof: bool ++ typeof: bool ++
Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration.
@ -120,7 +116,6 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge
"sway/workspaces": { "sway/workspaces": {
"disable-scroll": true, "disable-scroll": true,
"all-outputs": true, "all-outputs": true,
"numeric-first": false,
"format": "{name}: {icon}", "format": "{name}: {icon}",
"format-icons": { "format-icons": {
"1": "", "1": "",

View File

@ -93,10 +93,15 @@ Addressed by *wlr/taskbar*
# CLICK ACTIONS # CLICK ACTIONS
*activate*: Bring the application into foreground. *activate*: Bring the application into foreground.
*minimize*: Toggle application's minimized state. *minimize*: Toggle application's minimized state.
*minimize-raise*: Bring the application into foreground or toggle its minimized state. *minimize-raise*: Bring the application into foreground or toggle its minimized state.
*maximize*: Toggle application's maximized state. *maximize*: Toggle application's maximized state.
*fullscreen*: Toggle application's fullscreen state. *fullscreen*: Toggle application's fullscreen state.
*close*: Close the application. *close*: Close the application.
# EXAMPLES # EXAMPLES

View File

@ -52,6 +52,7 @@ Addressed by *wlr/workspaces*
# CLICK ACTIONS # CLICK ACTIONS
*activate*: Switch to workspace. *activate*: Switch to workspace.
*close*: Close the workspace. *close*: Close the workspace.
# ICONS # ICONS

View File

@ -1,6 +1,6 @@
project( project(
'waybar', 'cpp', 'c', 'waybar', 'cpp', 'c',
version: '0.9.8', version: '0.9.9',
license: 'MIT', license: 'MIT',
meson_version: '>= 0.49.0', meson_version: '>= 0.49.0',
default_options : [ default_options : [
@ -79,7 +79,7 @@ is_netbsd = host_machine.system() == 'netbsd'
is_openbsd = host_machine.system() == 'openbsd' is_openbsd = host_machine.system() == 'openbsd'
thread_dep = dependency('threads') thread_dep = dependency('threads')
fmt = dependency('fmt', version : ['>=5.3.0'], fallback : ['fmt', 'fmt_dep']) fmt = dependency('fmt', version : ['>=7.0.0'], fallback : ['fmt', 'fmt_dep'])
spdlog = dependency('spdlog', version : ['>=1.8.5'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=true']) spdlog = dependency('spdlog', version : ['>=1.8.5'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=true'])
wayland_client = dependency('wayland-client') wayland_client = dependency('wayland-client')
wayland_cursor = dependency('wayland-cursor') wayland_cursor = dependency('wayland-cursor')

View File

@ -1,10 +1,7 @@
* { * {
border: none;
border-radius: 0;
/* `otf-font-awesome` is required to be installed for icons */ /* `otf-font-awesome` is required to be installed for icons */
font-family: Roboto, Helvetica, Arial, sans-serif; font-family: Roboto, Helvetica, Arial, sans-serif;
font-size: 13px; font-size: 13px;
min-height: 0;
} }
window#waybar { window#waybar {
@ -43,6 +40,9 @@ window#waybar.chromium {
color: #ffffff; color: #ffffff;
/* Use box-shadow instead of border so the text isn't offset */ /* Use box-shadow instead of border so the text isn't offset */
box-shadow: inset 0 -3px transparent; box-shadow: inset 0 -3px transparent;
/* Avoid rounded borders under each workspace name */
border: none;
border-radius: 0;
} }
/* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */ /* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */

View File

@ -1,17 +1,24 @@
#include "modules/clock.hpp" #include "modules/clock.hpp"
#include <time.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#if FMT_VERSION < 60000
#include <fmt/time.h>
#else
#include <fmt/chrono.h>
#endif
#include <ctime>
#include <sstream> #include <sstream>
#include <type_traits> #include <type_traits>
#include "util/ustring_clen.hpp" #include "util/ustring_clen.hpp"
#include "util/waybar_time.hpp"
#ifdef HAVE_LANGINFO_1STDAY #ifdef HAVE_LANGINFO_1STDAY
#include <langinfo.h> #include <langinfo.h>
#include <locale.h> #include <locale.h>
#endif #endif
using waybar::modules::waybar_time; using waybar::waybar_time;
waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
: ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true),
@ -227,14 +234,3 @@ auto waybar::modules::Clock::first_day_of_week() -> date::weekday {
#endif #endif
return date::Sunday; return date::Sunday;
} }
template <>
struct fmt::formatter<waybar_time> : fmt::formatter<std::tm> {
template <typename FormatContext>
auto format(const waybar_time& t, FormatContext& ctx) {
#if FMT_VERSION >= 80000
auto& tm_format = specs;
#endif
return format_to(ctx.out(), "{}", date::format(t.locale, fmt::to_string(tm_format), t.ztime));
}
};

View File

@ -12,7 +12,15 @@ auto waybar::modules::Memory::update() -> void {
parseMeminfo(); parseMeminfo();
unsigned long memtotal = meminfo_["MemTotal"]; unsigned long memtotal = meminfo_["MemTotal"];
unsigned long swaptotal = 0;
if (meminfo_.count("SwapTotal")) {
swaptotal = meminfo_["SwapTotal"];
}
unsigned long memfree; unsigned long memfree;
unsigned long swapfree = 0;
if (meminfo_.count("SwapFree")) {
swapfree = meminfo_["SwapFree"];
}
if (meminfo_.count("MemAvailable")) { if (meminfo_.count("MemAvailable")) {
// New kernels (3.4+) have an accurate available memory field. // New kernels (3.4+) have an accurate available memory field.
memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"];
@ -24,9 +32,16 @@ auto waybar::modules::Memory::update() -> void {
if (memtotal > 0 && memfree >= 0) { if (memtotal > 0 && memfree >= 0) {
auto total_ram_gigabytes = memtotal / std::pow(1024, 2); auto total_ram_gigabytes = memtotal / std::pow(1024, 2);
auto total_swap_gigabytes = swaptotal / std::pow(1024, 2);
int used_ram_percentage = 100 * (memtotal - memfree) / memtotal; int used_ram_percentage = 100 * (memtotal - memfree) / memtotal;
int used_swap_percentage = 0;
if (swaptotal && swapfree) {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
}
auto used_ram_gigabytes = (memtotal - memfree) / std::pow(1024, 2); auto used_ram_gigabytes = (memtotal - memfree) / std::pow(1024, 2);
auto used_swap_gigabytes = (swaptotal - swapfree) / std::pow(1024, 2);
auto available_ram_gigabytes = memfree / std::pow(1024, 2); auto available_ram_gigabytes = memfree / std::pow(1024, 2);
auto available_swap_gigabytes = swapfree / std::pow(1024, 2);
auto format = format_; auto format = format_;
auto state = getState(used_ram_percentage); auto state = getState(used_ram_percentage);
@ -43,9 +58,13 @@ auto waybar::modules::Memory::update() -> void {
used_ram_percentage, used_ram_percentage,
fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("icon", getIcon(used_ram_percentage, icons)),
fmt::arg("total", total_ram_gigabytes), fmt::arg("total", total_ram_gigabytes),
fmt::arg("swapTotal", total_swap_gigabytes),
fmt::arg("percentage", used_ram_percentage), fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapPercentage", used_swap_percentage),
fmt::arg("used", used_ram_gigabytes), fmt::arg("used", used_ram_gigabytes),
fmt::arg("avail", available_ram_gigabytes))); fmt::arg("swapUsed", used_swap_gigabytes),
fmt::arg("avail", available_ram_gigabytes),
fmt::arg("swapAvail", available_swap_gigabytes)));
} }
if (tooltipEnabled()) { if (tooltipEnabled()) {
@ -54,9 +73,13 @@ auto waybar::modules::Memory::update() -> void {
label_.set_tooltip_text(fmt::format(tooltip_format, label_.set_tooltip_text(fmt::format(tooltip_format,
used_ram_percentage, used_ram_percentage,
fmt::arg("total", total_ram_gigabytes), fmt::arg("total", total_ram_gigabytes),
fmt::arg("swapTotal", total_swap_gigabytes),
fmt::arg("percentage", used_ram_percentage), fmt::arg("percentage", used_ram_percentage),
fmt::arg("swapPercentage", used_swap_percentage),
fmt::arg("used", used_ram_gigabytes), fmt::arg("used", used_ram_gigabytes),
fmt::arg("avail", available_ram_gigabytes))); fmt::arg("swapUsed", used_swap_gigabytes),
fmt::arg("avail", available_ram_gigabytes),
fmt::arg("swapAvail", available_swap_gigabytes)));
} else { } else {
label_.set_tooltip_text(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1)); label_.set_tooltip_text(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1));
} }

View File

@ -2,6 +2,7 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm>
#include <cctype> #include <cctype>
#include <string> #include <string>
@ -98,6 +99,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
Json::Value v; Json::Value v;
v["name"] = p_w_name; v["name"] = p_w_name;
v["target_output"] = bar_.output->name; v["target_output"] = bar_.output->name;
v["num"] = convertWorkspaceNameToNum(p_w_name);
workspaces_.emplace_back(std::move(v)); workspaces_.emplace_back(std::move(v));
break; break;
} }
@ -107,57 +109,59 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
Json::Value v; Json::Value v;
v["name"] = p_w_name; v["name"] = p_w_name;
v["target_output"] = ""; v["target_output"] = "";
v["num"] = convertWorkspaceNameToNum(p_w_name);
workspaces_.emplace_back(std::move(v)); workspaces_.emplace_back(std::move(v));
} }
} }
} }
// config option to sort numeric workspace names before others // sway has a defined ordering of workspaces that should be preserved in
bool config_numeric_first = config_["numeric-first"].asBool(); // the representation displayed by waybar to ensure that commands such
// as "workspace prev" or "workspace next" make sense when looking at
// the workspace representation in the bar.
// Due to waybar's own feature of persistent workspaces unknown to sway,
// custom sorting logic is necessary to make these workspaces appear
// naturally in the list of workspaces without messing up sway's
// sorting. For this purpose, a custom numbering property is created
// that preserves the order provided by sway while inserting numbered
// persistent workspaces at their natural positions.
//
// All of this code assumes that sway provides numbered workspaces first
// and other workspaces are sorted by their creation time.
//
// In a first pass, the maximum "num" value is computed to enqueue
// unnumbered workspaces behind numbered ones when computing the sort
// attribute.
int max_num = -1;
for (auto & workspace : workspaces_) {
max_num = std::max(workspace["num"].asInt(), max_num);
}
for (auto & workspace : workspaces_) {
auto workspace_num = workspace["num"].asInt();
if (workspace_num > -1) {
workspace["sort"] = workspace_num;
} else {
workspace["sort"] = ++max_num;
}
}
std::sort(workspaces_.begin(), std::sort(workspaces_.begin(),
workspaces_.end(), workspaces_.end(),
[config_numeric_first](const Json::Value &lhs, const Json::Value &rhs) { [](const Json::Value &lhs, const Json::Value &rhs) {
// the "num" property (integer type):
// The workspace number or -1 for workspaces that do
// not start with a number.
// We could rely on sway providing this property:
//
// auto l = lhs["num"].asInt();
// auto r = rhs["num"].asInt();
//
// We cannot rely on the "num" property as provided by sway
// via IPC, because persistent workspace might not exist in
// sway's view. However, we need this property also for
// not-yet created persistent workspace. As such, we simply
// duplicate sway's logic of assigning the "num" property
// into waybar (see convertWorkspaceNameToNum). This way the
// sorting should work out even when we include workspaces
// that do not currently exist.
auto lname = lhs["name"].asString(); auto lname = lhs["name"].asString();
auto rname = rhs["name"].asString(); auto rname = rhs["name"].asString();
int l = convertWorkspaceNameToNum(lname); int l = lhs["sort"].asInt();
int r = convertWorkspaceNameToNum(rname); int r = rhs["sort"].asInt();
if (l == r) { if (l == r) {
// in case both integers are the same, lexicographical // In case both integers are the same, lexicographical
// sort. This also covers the case when both don't have a // sort. The code above already ensure that this will only
// number (i.e., l == r == -1). // happend in case of explicitly numbered workspaces.
return lname < rname; return lname < rname;
} }
// one of the workspaces doesn't begin with a number, so
// num is -1.
if (l < 0 || r < 0) {
if (config_numeric_first) {
return r < 0;
}
return l < 0;
}
// both workspaces have a "num" so let's just compare those
return l < r; return l < r;
}); });
} }
dp.emit(); dp.emit();
} catch (const std::exception &e) { } catch (const std::exception &e) {

View File

@ -1,4 +1,3 @@
#define CATCH_CONFIG_RUNNER
#include "util/SafeSignal.hpp" #include "util/SafeSignal.hpp"
#include <glibmm.h> #include <glibmm.h>
@ -138,8 +137,3 @@ TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thr
producer.join(); producer.join();
REQUIRE(count == NUM_EVENTS); REQUIRE(count == NUM_EVENTS);
} }
int main(int argc, char* argv[]) {
Glib::init();
return Catch::Session().run(argc, argv);
}

View File

@ -1,4 +1,3 @@
#define CATCH_CONFIG_MAIN
#include "config.hpp" #include "config.hpp"
#include <catch2/catch.hpp> #include <catch2/catch.hpp>

26
test/main.cpp 100644
View File

@ -0,0 +1,26 @@
#define CATCH_CONFIG_RUNNER
#include <glibmm.h>
#include <spdlog/sinks/stdout_sinks.h>
#include <spdlog/spdlog.h>
#include <catch2/catch.hpp>
#include <catch2/catch_reporter_tap.hpp>
#include <memory>
int main(int argc, char* argv[]) {
Catch::Session session;
Glib::init();
session.applyCommandLine(argc, argv);
const auto logger = spdlog::default_logger();
const auto& reporter_name = session.config().getReporterName();
if (reporter_name == "tap") {
spdlog::set_pattern("# [%l] %v");
} else if (reporter_name == "compact") {
logger->sinks().clear();
} else {
logger->sinks().assign({std::make_shared<spdlog::sinks::stderr_sink_st>()});
}
return session.run();
}

View File

@ -6,30 +6,27 @@ test_dep = [
jsoncpp, jsoncpp,
spdlog, spdlog,
] ]
test_src = files(
config_test = executable( 'main.cpp',
'config_test', 'SafeSignal.cpp',
'config.cpp', 'config.cpp',
'../src/config.cpp', '../src/config.cpp',
dependencies: test_dep,
include_directories: test_inc,
) )
safesignal_test = executable( if tz_dep.found()
'safesignal_test', test_dep += tz_dep
'SafeSignal.cpp', test_src += files('waybar_time.cpp')
endif
waybar_test = executable(
'waybar_test',
test_src,
dependencies: test_dep, dependencies: test_dep,
include_directories: test_inc, include_directories: test_inc,
) )
test( test(
'Configuration test', 'waybar',
config_test, waybar_test,
workdir: meson.source_root(),
)
test(
'SafeSignal test',
safesignal_test,
workdir: meson.source_root(), workdir: meson.source_root(),
) )

View File

@ -0,0 +1,90 @@
#include "util/waybar_time.hpp"
#include <date/date.h>
#include <date/tz.h>
#include <catch2/catch.hpp>
#include <chrono>
#include <stdexcept>
using namespace std::literals::chrono_literals;
/*
* Check that the date/time formatter with locale and timezone support is working as expected.
*/
const date::zoned_time<std::chrono::seconds> TEST_TIME = date::make_zoned(
"UTC", date::local_days{date::Monday[1] / date::January / 2022} + 13h + 4min + 5s);
TEST_CASE("Format UTC time", "[clock][util]") {
waybar::waybar_time tm{std::locale("C"), TEST_TIME};
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE(fmt::format("{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405");
/* Test a few locales that are most likely to be present */
SECTION("US locale") {
try {
tm.locale = std::locale("en_US");
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM"));
REQUIRE(fmt::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405");
} catch (const std::runtime_error&) {
// locale not found; ignore
}
}
SECTION("GB locale") {
try {
tm.locale = std::locale("en_GB");
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 13:04:05"));
REQUIRE(fmt::format("{:%x %X}", tm) == "03/01/22 13:04:05");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405");
} catch (const std::runtime_error&) {
// locale not found; ignore
}
}
}
TEST_CASE("Format zoned time", "[clock][util]") {
waybar::waybar_time tm{std::locale("C"), date::make_zoned("America/New_York", TEST_TIME)};
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE(fmt::format("{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405");
/* Test a few locales that are most likely to be present */
SECTION("US locale") {
try {
tm.locale = std::locale("en_US");
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM"));
REQUIRE(fmt::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405");
} catch (const std::runtime_error&) {
// locale not found; ignore
}
}
SECTION("GB locale") {
try {
tm.locale = std::locale("en_GB");
REQUIRE(fmt::format("{}", tm).empty()); // no format specified
REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05"));
REQUIRE(fmt::format("{:%x %X}", tm) == "03/01/22 08:04:05");
REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405");
} catch (const std::runtime_error&) {
// locale not found; ignore
}
}
}