diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 19144aae..dffdb908 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,7 +31,7 @@ jobs:
cc: gcc-14
cxx: g++-14
ldflags: -fuse-ld=mold
- packages: g++-14 mold
+ packages: g++-14 mold libfmt-dev
meson_options:
- compiler: gcc11
os: ubuntu-22.04
@@ -39,13 +39,13 @@ jobs:
cxx: g++-11
ldflags:
packages: g++-11
- meson_options:
+ meson_options: --force-fallback-for=fmt
- compiler: clang
os: ubuntu-24.04
cc: clang
cxx: clang++
ldflags: -fuse-ld=lld
- packages: clang lld
+ packages: clang lld libfmt-dev
meson_options:
runs-on: ${{ matrix.os }}
diff --git a/NEWS b/NEWS
index 3b8edcdb..93697914 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,6 @@
ncmpc 0.50 - not yet released
* build: require Meson 0.60
+* require libfmt 9
* lyrics/musixmatch: add new lyrics extension
* lyrics/google: fix partial loading of lyrics
diff --git a/README.rst b/README.rst
index eee681e2..87a1e6d7 100644
--- a/README.rst
+++ b/README.rst
@@ -14,6 +14,7 @@ How to compile and install ncmpc
You need:
- a C++20 compliant compiler (e.g. gcc or clang)
+- `libfmt `__
- `libmpdclient `__ 2.16
- `ncurses `__
- `Meson 0.60 `__ and `Ninja `__
diff --git a/meson.build b/meson.build
index d0c80ed9..228a2c34 100644
--- a/meson.build
+++ b/meson.build
@@ -13,6 +13,7 @@ project('ncmpc', 'cpp',
)
cc = meson.get_compiler('cpp')
+compiler = cc
conf = configuration_data()
conf.set_quoted('PACKAGE', meson.project_name())
@@ -379,6 +380,7 @@ if host_machine.system() == 'windows'
subdir('src/win')
endif
+subdir('src/lib/fmt')
subdir('src/io')
subdir('src/system')
subdir('src/net')
@@ -451,6 +453,7 @@ ncmpc = executable('ncmpc',
curses_dep,
lirc_dep,
libmpdclient_dep,
+ fmt_dep,
],
install: true
)
diff --git a/src/ConfigParser.cxx b/src/ConfigParser.cxx
index 503a66c2..2db79ea7 100644
--- a/src/ConfigParser.cxx
+++ b/src/ConfigParser.cxx
@@ -15,9 +15,9 @@
#include "screen_list.hxx"
#include "PageMeta.hxx"
#include "Options.hxx"
+#include "lib/fmt/RuntimeError.hxx"
#include "util/CharUtil.hxx"
#include "util/PrintException.hxx"
-#include "util/RuntimeError.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringAPI.hxx"
#include "util/StringStrip.hxx"
@@ -105,8 +105,8 @@ static char *
after_unquoted_word(char *p)
{
if (!is_word_char(*p))
- throw FormatRuntimeError("%s: %s",
- _("Word expected"), p);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Word expected"), p);
++p;
@@ -149,8 +149,8 @@ NextUnquotedValue(char *&pp)
*end = 0;
pp = StripLeft(end + 1);
} else
- throw FormatRuntimeError("%s: %s",
- _("Whitespace expected"), end);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Whitespace expected"), end);
return value;
}
@@ -163,8 +163,8 @@ NextQuotedValue(char *&pp)
{
char *p = pp;
if (*p != '"')
- throw FormatRuntimeError("%s: %s",
- _("Quoted value expected"), p);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Quoted value expected"), p);
++p;
@@ -172,8 +172,8 @@ NextQuotedValue(char *&pp)
char *end = strchr(p, '"');
if (end == nullptr)
- throw FormatRuntimeError("%s: %s",
- _("Closing quote missing"), p);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Closing quote missing"), p);
*end = 0;
pp = end + 1;
@@ -190,8 +190,8 @@ NextNameValue(char *&p)
p = after_unquoted_word(p);
if (*p != '=')
- throw FormatRuntimeError("%s: %s",
- _("Syntax error"), p);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Syntax error"), p);
*p++ = 0;
@@ -210,9 +210,9 @@ parse_key_value(const char *str, const char **end)
{
auto result = ParseKeyName(str);
if (result.first == -1)
- throw FormatRuntimeError("%s: %s",
- _("Malformed hotkey definition"),
- result.second);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Malformed hotkey definition"),
+ result.second);
*end = result.second;
return result.first;
@@ -227,11 +227,11 @@ parse_key_definition(char *str)
/* get the command name */
char *eq = strchr(str, '=');
if (eq == nullptr)
- throw FormatRuntimeError("%s: %s",
- /* the hotkey configuration
- line is incomplete */
- _("Incomplete hotkey configuration"),
- str);
+ throw FmtRuntimeError("{}: {:?}",
+ /* the hotkey configuration line
+ is incomplete */
+ _("Incomplete hotkey configuration"),
+ str);
char *command_name = str;
str = StripLeft(eq + 1);
@@ -240,11 +240,11 @@ parse_key_definition(char *str)
StripRight(command_name);
const auto cmd = get_key_command_from_name(command_name);
if (cmd == Command::NONE)
- throw FormatRuntimeError("%s: %s",
- /* the hotkey configuration
- contains an unknown
- command */
- _("Unknown command"), command_name);
+ throw FmtRuntimeError("{}: {:?}",
+ /* the hotkey configuration
+ contains an unknown
+ command */
+ _("Unknown command"), command_name);
/* parse key values */
size_t i = 0;
@@ -273,16 +273,15 @@ ParseCurrentTimeDisplay(const char *str)
else if (StringIsEqual(str, "none"))
return CurrentTimeDisplay::NONE;
else
- throw FormatRuntimeError("%s: %s",
- /* translators: ncmpc
- supports displaying the
- "elapsed" or "remaining"
- time of a song being
- played; in this case, the
- configuration file
- contained an invalid
- setting */
- _("Bad time display type"), str);
+ throw FmtRuntimeError("{}: {:?}",
+ /* translators: ncmpc supports
+ displaying the "elapsed" or
+ "remaining" time of a song
+ being played; in this case,
+ the configuration file
+ contained an invalid
+ setting */
+ _("Bad time display type"), str);
}
#ifdef ENABLE_COLORS
@@ -344,8 +343,8 @@ parse_color_definition(char *str)
/* get the command name */
short color = ParseColorNameOrNumber(str);
if (color < 0)
- throw FormatRuntimeError("%s: %s",
- _("Bad color name"), str);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Bad color name"), str);
/* parse r,g,b values */
@@ -353,21 +352,21 @@ parse_color_definition(char *str)
for (unsigned i = 0; i < 3; ++i) {
char *next = after_comma(value), *endptr;
if (*value == 0)
- throw FormatRuntimeError("%s: %s",
- _("Incomplete color definition"),
- str);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Incomplete color definition"),
+ str);
rgb[i] = strtol(value, &endptr, 0);
if (endptr == value || *endptr != 0)
- throw FormatRuntimeError("%s: %s",
- _("Invalid number"), value);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Invalid number"), value);
value = next;
}
if (*value != 0)
- throw FormatRuntimeError("%s: %s",
- _("Malformed color definition"), str);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Malformed color definition"), str);
colors_define(color, rgb[0], rgb[1], rgb[2]);
}
@@ -425,14 +424,13 @@ check_screen_list(char *value)
const auto *page_meta = screen_lookup_name(name);
if (page_meta == nullptr)
- throw FormatRuntimeError("%s: %s",
- /* an unknown screen
- name was specified
- in the
- configuration
- file */
- _("Unknown screen name"),
- name);
+ throw FmtRuntimeError("{}: {:?}",
+ /* an unknown screen
+ name was specified in
+ the configuration
+ file */
+ _("Unknown screen name"),
+ name);
/* use PageMeta::name because
screen_lookup_name() may have translated a
@@ -459,8 +457,8 @@ ParseTagList(char *value)
while (char *name = NextItem(value)) {
auto type = mpd_tag_name_iparse(name);
if (type == MPD_TAG_UNKNOWN)
- throw FormatRuntimeError("%s: %s",
- _("Unknown MPD tag"), name);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Unknown MPD tag"), name);
result.emplace_back(type);
}
@@ -486,8 +484,8 @@ get_search_mode(char *value)
if (0 <= mode && mode <= 4)
return mode;
else
- throw FormatRuntimeError("%s: %s",
- _("Invalid search mode"),value);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Invalid search mode"),value);
}
else
{
@@ -504,9 +502,9 @@ get_search_mode(char *value)
else if (StringIsEqualIgnoreCase(value, "artist+album"))
return 4;
else
- throw FormatRuntimeError("%s: %s",
- _("Unknown search mode"),
- value);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Unknown search mode"),
+ value);
}
}
@@ -537,18 +535,18 @@ ParseTableColumn(char *s)
column.min_width = strtoul(value, &endptr, 10);
if (endptr == value || *endptr != 0 ||
column.min_width == 0 || column.min_width > 1000)
- throw FormatRuntimeError("%s: %s",
- _("Invalid column width"),
- value);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Invalid column width"),
+ value);
} else if (StringIsEqual(name, "fraction")) {
char *endptr;
column.fraction_width = strtod(value, &endptr);
if (endptr == value || *endptr != 0 ||
column.fraction_width < 0 ||
column.fraction_width > 1000)
- throw FormatRuntimeError("%s: %s",
- _("Invalid column fraction width"),
- value);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Invalid column fraction width"),
+ value);
}
}
@@ -581,8 +579,8 @@ parse_line(char *line)
++line;
line = StripLeft(line);
} else if (line == name_end) {
- throw FormatRuntimeError("%s: %s",
- _("Missing '='"), name_end);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Missing '='"), name_end);
}
*name_end = 0;
@@ -753,9 +751,9 @@ parse_line(char *line)
options.second_column = str2bool(value);
#endif
else
- throw FormatRuntimeError("%s: %s",
- _("Unknown configuration parameter"),
- name);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Unknown configuration parameter"),
+ name);
}
bool
@@ -784,9 +782,9 @@ ReadConfigFile(const char *filename)
try {
parse_line(p);
} catch (...) {
- fprintf(stderr,
- "Failed to parse '%s' line %u: ",
- filename, no);
+ fmt::print(stderr,
+ "Failed to parse {:?} line {}: ",
+ filename, no);
PrintException(std::current_exception());
}
}
diff --git a/src/EditPlaylistPage.cxx b/src/EditPlaylistPage.cxx
index ce3bc1f1..3bae7b79 100644
--- a/src/EditPlaylistPage.cxx
+++ b/src/EditPlaylistPage.cxx
@@ -12,12 +12,14 @@
#include "Options.hxx"
#include "mpdclient.hxx"
#include "screen.hxx"
-#include "util/SPrintf.hxx"
+#include "lib/fmt/ToSpan.hxx"
#include
#include
+using std::string_view_literals::operator""sv;
+
static std::string next_playlist_name;
class EditPlaylistPage final : public FileListPage {
@@ -119,7 +121,7 @@ EditPlaylistPage::GetTitle(std::span buffer) const noexcept
if (name.empty())
return _("Playlist");
- return SPrintf(buffer, "%s: %s", _("Playlist"), name.c_str());
+ return FmtTruncate(buffer, "{}: {}"sv, _("Playlist"), name);
}
void
diff --git a/src/FileBrowserPage.cxx b/src/FileBrowserPage.cxx
index 39f358a4..5d7d7cad 100644
--- a/src/FileBrowserPage.cxx
+++ b/src/FileBrowserPage.cxx
@@ -16,7 +16,7 @@
#include "screen_client.hxx"
#include "Command.hxx"
#include "Options.hxx"
-#include "util/SPrintf.hxx"
+#include "lib/fmt/ToSpan.hxx"
#include "util/UriUtil.hxx"
#include
@@ -286,9 +286,9 @@ FileBrowserPage::GetTitle(std::span buffer) const noexcept
/* fall back to full path */
path = current_path.c_str();
- return SPrintf(buffer, "%s: %s",
- /* translators: caption of the browser screen */
- _("Browse"), Utf8ToLocale(path).c_str());
+ return FmtTruncate(buffer, "{}: {}",
+ /* translators: caption of the browser screen */
+ _("Browse"), Utf8ToLocale(path).c_str());
}
void
diff --git a/src/LibraryPage.cxx b/src/LibraryPage.cxx
index 062e7b11..4b6624bb 100644
--- a/src/LibraryPage.cxx
+++ b/src/LibraryPage.cxx
@@ -12,7 +12,7 @@
#include "mpdclient.hxx"
#include "filelist.hxx"
#include "Options.hxx"
-#include "util/SPrintf.hxx"
+#include "lib/fmt/ToSpan.hxx"
#include
#include
@@ -21,6 +21,8 @@
#include
#include
+using std::string_view_literals::operator""sv;
+
[[gnu::const]]
static const char *
GetTagPlural(enum mpd_tag_type tag) noexcept
@@ -38,14 +40,14 @@ GetTagPlural(enum mpd_tag_type tag) noexcept
}
static std::string_view
-MakePageTitle(std::span buffer, const char *prefix,
+MakePageTitle(std::span buffer, std::string_view prefix,
const TagFilter &filter)
{
if (filter.empty())
return prefix;
- return SPrintf(buffer, "%s: %s", prefix,
- Utf8ToLocale(ToString(filter).c_str()).c_str());
+ return FmtTruncate(buffer, "{}: {}"sv, prefix,
+ Utf8ToLocale{ToString(filter)}.c_str());
}
class SongListPage final : public FileListPage {
diff --git a/src/LyricsPage.cxx b/src/LyricsPage.cxx
index 7618f01b..6d7a24c1 100644
--- a/src/LyricsPage.cxx
+++ b/src/LyricsPage.cxx
@@ -17,7 +17,7 @@
#include "TextPage.hxx"
#include "screen_utils.hxx"
#include "ncu.hxx"
-#include "util/SPrintf.hxx"
+#include "lib/fmt/ToSpan.hxx"
#include "util/StringAPI.hxx"
#include
@@ -313,11 +313,12 @@ std::string_view
LyricsPage::GetTitle(std::span buffer) const noexcept
{
if (plugin_cycle != nullptr) {
- return SPrintf(buffer, "%s (%s)",
- _("Lyrics"),
- /* translators: this message is displayed
- while data is retrieved */
- _("loading..."));
+ return FmtTruncate(buffer, "{} ({})",
+ _("Lyrics"),
+ /* translators: this message is
+ displayed while data is
+ retrieved */
+ _("loading..."));
} else if (artist != nullptr && title != nullptr && !IsEmpty()) {
std::size_t n;
n = snprintf(buffer.data(), buffer.size(), "%s: %s - %s",
diff --git a/src/SearchPage.cxx b/src/SearchPage.cxx
index 8a0dad25..021f8c83 100644
--- a/src/SearchPage.cxx
+++ b/src/SearchPage.cxx
@@ -14,13 +14,15 @@
#include "screen_utils.hxx"
#include "FileListPage.hxx"
#include "filelist.hxx"
-#include "util/SPrintf.hxx"
+#include "lib/fmt/ToSpan.hxx"
#include "util/StringAPI.hxx"
#include
#include
+using std::string_view_literals::operator""sv;
+
enum {
SEARCH_URI = MPD_TAG_COUNT + 100,
SEARCH_MODIFIED,
@@ -131,15 +133,15 @@ class SearchHelpText final : public ListText {
assert(idx < std::size(help_text));
if (idx == 0)
- return SPrintf(buffer, " %s : %s",
- GetGlobalKeyBindings().GetKeyNames(Command::SCREEN_SEARCH).c_str(),
- "New search");
+ return FmtTruncate(buffer, " {} : {}"sv,
+ GetGlobalKeyBindings().GetKeyNames(Command::SCREEN_SEARCH),
+ "New search"sv);
if (idx == 1)
- return SPrintf(buffer, " %s : %s [%s]",
- GetGlobalKeyBindings().GetKeyNames(Command::SEARCH_MODE).c_str(),
- get_key_description(Command::SEARCH_MODE),
- my_gettext(mode[options.search_mode].label));
+ return FmtTruncate(buffer, " {} : {} [{}]"sv,
+ GetGlobalKeyBindings().GetKeyNames(Command::SEARCH_MODE),
+ get_key_description(Command::SEARCH_MODE),
+ my_gettext(mode[options.search_mode].label));
return help_text[idx];
}
@@ -442,12 +444,12 @@ std::string_view
SearchPage::GetTitle(std::span buffer) const noexcept
{
if (advanced_search_mode && !pattern.empty())
- return SPrintf(buffer, "%s '%s'", _("Search"), pattern.c_str());
+ return FmtTruncate(buffer, "{} '{}'"sv, _("Search"), pattern);
else if (!pattern.empty())
- return SPrintf(buffer, "%s '%s' [%s]",
- _("Search"),
- pattern.c_str(),
- my_gettext(mode[options.search_mode].label));
+ return FmtTruncate(buffer, "{} '{}' [{}]",
+ _("Search"),
+ pattern,
+ my_gettext(mode[options.search_mode].label));
else
return _("Search");
}
diff --git a/src/Styles.cxx b/src/Styles.cxx
index c2f706e4..119fba2a 100644
--- a/src/Styles.cxx
+++ b/src/Styles.cxx
@@ -5,7 +5,7 @@
#include "BasicColors.hxx"
#include "CustomColors.hxx"
#include "i18n.h"
-#include "util/RuntimeError.hxx"
+#include "lib/fmt/RuntimeError.hxx"
#include "util/StringAPI.hxx"
#include "util/StringStrip.hxx"
#include "Window.hxx"
@@ -232,7 +232,7 @@ ParseBackgroundColor(const char *s)
if (StringIsEqualIgnoreCase(s, "none"))
return COLOR_NONE;
- throw FormatRuntimeError("%s: %s", _("Unknown color"), s);
+ throw FmtRuntimeError("{}: {:?}", _("Unknown color"), s);
}
/**
@@ -292,8 +292,8 @@ ParseStyle(StyleData &d, const char *str)
else if (StringIsEqualIgnoreCase(cur, "bold"))
d.attr |= A_BOLD;
else
- throw FormatRuntimeError("%s: %s",
- _("Unknown color"), str);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Unknown color"), str);
}
}
@@ -302,8 +302,8 @@ ModifyStyle(const char *name, const char *value)
{
const auto style = StyleByName(name);
if (style == Style::END)
- throw FormatRuntimeError("%s: %s",
- _("Unknown color field"), name);
+ throw FmtRuntimeError("{}: {:?}",
+ _("Unknown color field"), name);
auto &data = GetStyle(style);
diff --git a/src/lib/fmt/RuntimeError.cxx b/src/lib/fmt/RuntimeError.cxx
new file mode 100644
index 00000000..27c55a21
--- /dev/null
+++ b/src/lib/fmt/RuntimeError.cxx
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// author: Max Kellermann
+
+#include "RuntimeError.hxx"
+#include "ToBuffer.hxx"
+
+std::runtime_error
+VFmtRuntimeError(fmt::string_view format_str, fmt::format_args args) noexcept
+{
+ const auto msg = VFmtBuffer<512>(format_str, args);
+ return std::runtime_error{msg};
+}
+
+std::invalid_argument
+VFmtInvalidArgument(fmt::string_view format_str, fmt::format_args args) noexcept
+{
+ const auto msg = VFmtBuffer<512>(format_str, args);
+ return std::invalid_argument{msg};
+}
diff --git a/src/lib/fmt/RuntimeError.hxx b/src/lib/fmt/RuntimeError.hxx
new file mode 100644
index 00000000..61623ec3
--- /dev/null
+++ b/src/lib/fmt/RuntimeError.hxx
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// author: Max Kellermann
+
+#pragma once
+
+#include
+
+#include // IWYU pragma: export
+
+[[nodiscard]] [[gnu::pure]]
+std::runtime_error
+VFmtRuntimeError(fmt::string_view format_str, fmt::format_args args) noexcept;
+
+template
+[[nodiscard]] [[gnu::pure]]
+auto
+FmtRuntimeError(const S &format_str, Args&&... args) noexcept
+{
+ return VFmtRuntimeError(format_str,
+ fmt::make_format_args(args...));
+}
+
+[[nodiscard]] [[gnu::pure]]
+std::invalid_argument
+VFmtInvalidArgument(fmt::string_view format_str, fmt::format_args args) noexcept;
+
+template
+[[nodiscard]] [[gnu::pure]]
+auto
+FmtInvalidArgument(const S &format_str, Args&&... args) noexcept
+{
+ return VFmtInvalidArgument(format_str,
+ fmt::make_format_args(args...));
+}
diff --git a/src/lib/fmt/ToBuffer.hxx b/src/lib/fmt/ToBuffer.hxx
new file mode 100644
index 00000000..01f6102f
--- /dev/null
+++ b/src/lib/fmt/ToBuffer.hxx
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// author: Max Kellermann
+
+#pragma once
+
+#include "util/StringBuffer.hxx"
+
+#include
+
+template
+StringBuffer &
+VFmtToBuffer(StringBuffer &buffer,
+ fmt::string_view format_str, fmt::format_args args) noexcept
+{
+ auto [p, _] = fmt::vformat_to_n(buffer.begin(), buffer.capacity() - 1,
+ format_str, args);
+ *p = 0;
+ return buffer;
+}
+
+template
+[[nodiscard]] [[gnu::pure]]
+auto
+VFmtBuffer(fmt::string_view format_str, fmt::format_args args) noexcept
+{
+ StringBuffer buffer;
+ return VFmtToBuffer(buffer, format_str, args);
+}
+
+template
+StringBuffer &
+FmtToBuffer(StringBuffer &buffer,
+ const S &format_str, Args&&... args) noexcept
+{
+ return VFmtToBuffer(buffer, format_str,
+ fmt::make_format_args(args...));
+}
+
+template
+[[nodiscard]] [[gnu::pure]]
+auto
+FmtBuffer(const S &format_str, Args&&... args) noexcept
+{
+ return VFmtBuffer(format_str,
+ fmt::make_format_args(args...));
+}
diff --git a/src/lib/fmt/ToSpan.hxx b/src/lib/fmt/ToSpan.hxx
new file mode 100644
index 00000000..bc9239c7
--- /dev/null
+++ b/src/lib/fmt/ToSpan.hxx
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// author: Max Kellermann
+
+#pragma once
+
+#include
+
+#include
+#include
+
+[[nodiscard]] [[gnu::pure]]
+inline std::string_view
+VFmtTruncate(std::span buffer,
+ fmt::string_view format_str, fmt::format_args args) noexcept
+{
+ auto [p, _] = fmt::vformat_to_n(buffer.begin(), buffer.size(),
+ format_str, args);
+ return {buffer.begin(), p};
+}
+
+template
+[[nodiscard]] [[gnu::pure]]
+inline std::string_view
+FmtTruncate(std::span buffer, const S &format_str, Args&&... args) noexcept
+{
+ return VFmtTruncate(buffer, format_str, fmt::make_format_args(args...));
+}
diff --git a/src/lib/fmt/meson.build b/src/lib/fmt/meson.build
new file mode 100644
index 00000000..4e4d9269
--- /dev/null
+++ b/src/lib/fmt/meson.build
@@ -0,0 +1,24 @@
+# using include_type:system to work around -Wfloat-equal
+libfmt = dependency('fmt', version: '>= 9',
+ include_type: 'system',
+ fallback: ['fmt', 'fmt_dep'])
+
+if compiler.get_id() == 'gcc' and compiler.version().version_compare('>=13') and compiler.version().version_compare('<15')
+ libfmt = declare_dependency(
+ dependencies: libfmt,
+ # suppress bogus GCC 13 warnings: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109717
+ compile_args: ['-Wno-array-bounds', '-Wno-stringop-overflow']
+ )
+endif
+
+fmt = static_library(
+ 'fmt',
+ 'RuntimeError.cxx',
+ include_directories: inc,
+ dependencies: libfmt,
+)
+
+fmt_dep = declare_dependency(
+ link_with: fmt,
+ dependencies: libfmt,
+)
diff --git a/src/net/Resolver.cxx b/src/net/Resolver.cxx
index 016fc906..ec94baa1 100644
--- a/src/net/Resolver.cxx
+++ b/src/net/Resolver.cxx
@@ -5,7 +5,7 @@
#include "Resolver.hxx"
#include "AddressInfo.hxx"
#include "HostParser.hxx"
-#include "util/RuntimeError.hxx"
+#include "lib/fmt/RuntimeError.hxx"
#include "util/CharUtil.hxx"
#include "util/StringAPI.hxx"
@@ -26,10 +26,10 @@ Resolve(const char *node, const char *service,
struct addrinfo *ai;
int error = getaddrinfo(node, service, hints, &ai);
if (error != 0)
- throw FormatRuntimeError("Failed to resolve '%s':'%s': %s",
- node == nullptr ? "" : node,
- service == nullptr ? "" : service,
- gai_strerror(error));
+ throw FmtRuntimeError("Failed to resolve {:?}:{:?}: {}",
+ node == nullptr ? "" : node,
+ service == nullptr ? "" : service,
+ gai_strerror(error));
return AddressInfoList(ai);
}
@@ -60,7 +60,7 @@ FindAndResolveInterfaceName(char *host, size_t size)
const unsigned i = if_nametoindex(interface);
if (i == 0)
- throw FormatRuntimeError("No such interface: %s", interface);
+ throw FmtRuntimeError("No such interface: {}", interface);
sprintf(interface, "%u", i);
}
diff --git a/src/net/meson.build b/src/net/meson.build
index 4c234053..d552fbfb 100644
--- a/src/net/meson.build
+++ b/src/net/meson.build
@@ -17,6 +17,9 @@ net = static_library(
'SocketAddress.cxx',
'SocketDescriptor.cxx',
include_directories: inc,
+ dependencies: [
+ fmt_dep,
+ ],
)
net_dep = declare_dependency(
diff --git a/src/util/RuntimeError.hxx b/src/util/RuntimeError.hxx
deleted file mode 100644
index 94b7c6ee..00000000
--- a/src/util/RuntimeError.hxx
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-License-Identifier: BSD-2-Clause
-// author: Max Kellermann
-
-#ifndef RUNTIME_ERROR_HXX
-#define RUNTIME_ERROR_HXX
-
-#include // IWYU pragma: export
-#include
-
-#include
-
-#if defined(__clang__) || defined(__GNUC__)
-#pragma GCC diagnostic push
-// TODO: fix this warning properly
-#pragma GCC diagnostic ignored "-Wformat-security"
-#endif
-
-template
-static inline std::runtime_error
-FormatRuntimeError(const char *fmt, Args&&... args) noexcept
-{
- char buffer[1024];
- snprintf(buffer, sizeof(buffer), fmt, std::forward(args)...);
- return std::runtime_error(buffer);
-}
-
-template
-inline std::invalid_argument
-FormatInvalidArgument(const char *fmt, Args&&... args) noexcept
-{
- char buffer[1024];
- snprintf(buffer, sizeof(buffer), fmt, std::forward(args)...);
- return std::invalid_argument(buffer);
-}
-
-#if defined(__clang__) || defined(__GNUC__)
-#pragma GCC diagnostic pop
-#endif
-
-#endif
diff --git a/src/util/StringBuffer.hxx b/src/util/StringBuffer.hxx
new file mode 100644
index 00000000..e25d42cb
--- /dev/null
+++ b/src/util/StringBuffer.hxx
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// author: Max Kellermann
+
+#pragma once
+
+#include
+
+/**
+ * A statically allocated string buffer.
+ */
+template
+class BasicStringBuffer {
+public:
+ using value_type = T;
+ using reference = T &;
+ using pointer = T *;
+ using const_pointer = const T *;
+ using size_type = std::size_t;
+
+ static constexpr value_type SENTINEL = '\0';
+
+protected:
+ using Array = std::array;
+ Array the_data;
+
+public:
+ using iterator = typename Array::iterator;
+ using const_iterator = typename Array::const_iterator;
+
+ static constexpr size_type capacity() noexcept {
+ return CAPACITY;
+ }
+
+ constexpr bool empty() const noexcept {
+ return front() == SENTINEL;
+ }
+
+ constexpr void clear() noexcept {
+ the_data[0] = SENTINEL;
+ }
+
+ constexpr const_pointer c_str() const noexcept {
+ return the_data.data();
+ }
+
+ constexpr pointer data() noexcept {
+ return the_data.data();
+ }
+
+ constexpr value_type front() const noexcept {
+ return the_data.front();
+ }
+
+ /**
+ * Returns one character. No bounds checking.
+ */
+ constexpr value_type operator[](size_type i) const noexcept {
+ return the_data[i];
+ }
+
+ /**
+ * Returns one writable character. No bounds checking.
+ */
+ constexpr reference operator[](size_type i) noexcept {
+ return the_data[i];
+ }
+
+ constexpr iterator begin() noexcept {
+ return the_data.begin();
+ }
+
+ constexpr iterator end() noexcept {
+ return the_data.end();
+ }
+
+ constexpr const_iterator begin() const noexcept {
+ return the_data.begin();
+ }
+
+ constexpr const_iterator end() const noexcept {
+ return the_data.end();
+ }
+
+ constexpr operator const_pointer() const noexcept {
+ return c_str();
+ }
+};
+
+template
+class StringBuffer : public BasicStringBuffer {};
diff --git a/subprojects/.gitignore b/subprojects/.gitignore
index f6c4ee43..877887fc 100644
--- a/subprojects/.gitignore
+++ b/subprojects/.gitignore
@@ -1 +1,4 @@
+/packagecache/
+
+/fmt-*/
/libmpdclient/
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
new file mode 100644
index 00000000..42b61596
--- /dev/null
+++ b/subprojects/fmt.wrap
@@ -0,0 +1,13 @@
+[wrap-file]
+directory = fmt-11.0.1
+source_url = https://github.com/fmtlib/fmt/archive/11.0.1.tar.gz
+source_filename = fmt-11.0.1.tar.gz
+source_hash = 7d009f7f89ac84c0a83f79ed602463d092fbf66763766a907c97fd02b100f5e9
+patch_filename = fmt_11.0.1-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/fmt_11.0.1-1/get_patch
+patch_hash = 0a8b93d1ee6d84a82d3872a9bfb4c3977d8a53f7f484d42d1f7ed63ed496d549
+source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_11.0.1-1/fmt-11.0.1.tar.gz
+wrapdb_version = 11.0.1-1
+
+[provide]
+fmt = fmt_dep