diff --git a/data/nickserv.example.conf b/data/nickserv.example.conf index fc1609c9c..a36d49a18 100644 --- a/data/nickserv.example.conf +++ b/data/nickserv.example.conf @@ -695,6 +695,17 @@ module { name = "ns_set_language" } command { service = "NickServ"; name = "SET LANGUAGE"; command = "nickserv/set/language"; } command { service = "NickServ"; name = "SASET LANGUAGE"; command = "nickserv/saset/language"; permission = "nickserv/saset/language"; } +/* + * ns_set_layout + * + * Provides the command nickserv/set/layout and nickserv/saset/layout. + * + * Allows configuring the layout that services uses. + */ +module { name = "ns_set_layout" } +command { service = "NickServ"; name = "SET layout"; command = "nickserv/set/layout"; } +command { service = "NickServ"; name = "SASET layout"; command = "nickserv/saset/layout"; permission = "nickserv/saset/layout"; } + /* * ns_set_message * diff --git a/include/textproc.h b/include/textproc.h index ec321d6a7..21f9009c4 100644 --- a/include/textproc.h +++ b/include/textproc.h @@ -139,11 +139,15 @@ class CoreExport ListFormatter final { public: using ListEntry = std::map; + using FlexibleFormatFn = std::function; private: std::vector columns; std::vector entries; + FlexibleFormatFn flexiblerow; NickCore *nc; + void SendFixed(CommandSource &source); + void SendFlexible(CommandSource &source); public: ListFormatter(NickCore *nc); @@ -151,4 +155,6 @@ public: void AddEntry(const ListEntry &entry); bool IsEmpty() const; void SendTo(CommandSource &source); + void SetFlexible(const Anope::string &format); + void SetFlexible(const FlexibleFormatFn &formatter); }; diff --git a/language/anope.en_US.po b/language/anope.en_US.po index 565351168..0eb480110 100644 --- a/language/anope.en_US.po +++ b/language/anope.en_US.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Anope\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-12 15:43+0100\n" -"PO-Revision-Date: 2025-09-12 15:43+0100\n" +"POT-Creation-Date: 2025-09-19 18:50+0100\n" +"PO-Revision-Date: 2025-09-19 18:50+0100\n" "Last-Translator: Sadie Powell \n" "Language-Team: English\n" "Language: en_US\n" @@ -342,6 +342,63 @@ msgstr "" msgid "[auto-memo] The memo you sent to %s has been viewed." msgstr "" +msgid "{host}: {session} sessions" +msgstr "" + +msgid "{mask}" +msgstr "" + +msgid "{mask} -- created by {creator}; {expires}" +msgstr "" + +msgid "{mask} -- created by {creator}; {expires} ({reason})" +msgstr "" + +msgid "{mask} on {type} -- created by {creator}; expires in {expires}" +msgstr "" + +msgid "{mask} on {type} -- created by {creator}; expires in {expires} ({reason})" +msgstr "" + +msgid "{name}" +msgstr "" + +msgid "{name} ({description})" +msgstr "" + +msgid "{name} ({mask}) [{realname}]" +msgstr "" + +msgid "{name} -- {users} user(s); +{modes}" +msgstr "" + +msgid "{name} -- {users} user(s); +{modes} ({topic})" +msgstr "" + +msgid "{name} = {level}" +msgstr "" + +msgid "{name} = {value}" +msgstr "" + +msgid "{name}: {description}" +msgstr "" + +msgid "{nick}" +msgstr "" + +msgid "{nick} (last mask: {last_mask})" +msgstr "" + +msgid "{nick} ({mask}) [{real_name}]" +msgstr "" + +msgid "{nick}: expires in {expires}" +msgstr "" + +msgid "{}{module_name}}:{name} = {value}" +msgstr "" + msgid "[target] [password]" msgstr "" @@ -600,6 +657,9 @@ msgstr "" msgid "nickname {EMAIL | STATUS | MASK | QUIT} {ON | OFF}" msgstr "" +msgid "nickname {FIXED | FLEXIBLE}" +msgstr "" + msgid "nickname {ON | delay | OFF}" msgstr "" @@ -749,6 +809,10 @@ msgstr[1] "" msgid "%lu nicks are stored in the database, using %.2Lf kB of memory." msgstr "" +#, c-format +msgid "%s %s %s" +msgstr "" + #, c-format msgid "%s %s list is empty." msgstr "" @@ -1070,6 +1134,14 @@ msgstr "" msgid "%s's memo limit is %d." msgstr "" +#, c-format +msgid "%s: %s" +msgstr "" + +#, c-format +msgid "%s: %s %s" +msgstr "" + #, c-format msgid "%u channel" msgid_plural "%u channels" @@ -1906,9 +1978,6 @@ msgstr "" msgid "Bot won't kick for repeats anymore." msgstr "" -msgid "By" -msgstr "" - msgid "CLEAR target" msgstr "" @@ -2199,6 +2268,15 @@ msgstr "" msgid "Configures reverses kicker" msgstr "" +msgid "Configures the layout used by the account for services messages. When the layout is set to FIXED services will use tables and position text such that it looks good in a client that uses a fixed-width font. When the layout is set to FLEXIBLE services will use an alternate format for messages and avoid any positioning that might be broken by a variable-width font." +msgstr "" + +msgid "Configures the layout used for services messages" +msgstr "" + +msgid "Configures the layout used for services messages. When the layout is set to FIXED services will use tables and position text such that it looks good in a client that uses a fixed-width font. When the layout is set to FLEXIBLE services will use an alternate format for messages and avoid any positioning that might be broken by a variable-width font." +msgstr "" + msgid "Configures the time bot bans expire in" msgstr "" @@ -2881,6 +2959,9 @@ msgstr "" msgid "Fingerprint %s is already in use." msgstr "" +msgid "Fixed layout" +msgstr "" + msgid "Flags" msgstr "" @@ -2892,6 +2973,9 @@ msgstr "" msgid "Flags list for %s" msgstr "" +msgid "Flexible layout" +msgstr "" + msgid "Flood kicker" msgstr "" @@ -3151,6 +3235,14 @@ msgstr "" msgid "Last used" msgstr "" +#, c-format +msgid "Layout is now fixed for %s." +msgstr "" + +#, c-format +msgid "Layout is now flexible for %s." +msgstr "" + msgid "Level" msgstr "" @@ -4497,6 +4589,9 @@ msgstr "" msgid "Server %s must be quit before it can be deleted." msgstr "" +msgid "Server: {server} = {ip} -- limit: {limit}; state: {state}" +msgstr "" + msgid "Servers" msgstr "" @@ -6314,6 +6409,12 @@ msgstr "" msgid "Zone %s removed." msgstr "" +msgid "Zone: {zone}" +msgstr "" + +msgid "Zone: {zone} = {servers}" +msgstr "" + msgid "[1|2|3|4|5]" msgstr "" @@ -6447,3 +6548,87 @@ msgstr "" msgid "{ON | delay | OFF}" msgstr "" + +msgid "{mode} -- created by {creator} on {created}" +msgstr "" + +msgid "{mode} {param} -- created by {creator} on {created}" +msgstr "" + +msgid "{number}: {channel}" +msgstr "" + +msgid "{number}: {channel} (key: {key})" +msgstr "" + +msgid "{number}: {channel} = {access}" +msgstr "" + +msgid "{number}: {channel} = {access} ({description})" +msgstr "" + +msgid "{number}: {mask}" +msgstr "" + +msgid "{number}: {mask} ({description})" +msgstr "" + +msgid "{number}: {mask} ({reason})" +msgstr "" + +msgid "{number}: {mask} -- added by {creator} on {created}; last used: {last_used}" +msgstr "" + +msgid "{number}: {mask} -- added by {creator} on {created}; last used: {last_used} ({reason})" +msgstr "" + +msgid "{number}: {mask} -- created by {creator} on {created}; {expires} ({reason})" +msgstr "" + +msgid "{number}: {mask} -- {limit} sessions" +msgstr "" + +msgid "{number}: {mask} -- {limit} sessions; created by {creator} on {created}; {expires} ({reason})" +msgstr "" + +msgid "{number}: {mask} = {flags} -- added by {creator} at {created}" +msgstr "" + +msgid "{number}: {mask} = {flags} -- added by {creator} at {created} ({description})" +msgstr "" + +msgid "{number}: {mask} = {level}" +msgstr "" + +msgid "{number}: {mask} = {level} ({description})" +msgstr "" + +msgid "{number}: {mask} = {level} -- created by {creator}; last seen {last_seen}" +msgstr "" + +msgid "{number}: {mask} = {level} -- created by {creator}; last seen {last_seen} ({description})" +msgstr "" + +msgid "{number}: {nick} = {vhost} -- created by {creator} at {created}" +msgstr "" + +msgid "{number}: {word} -- type: {type}" +msgstr "" + +msgid "{number}: [{id}] {mask} -- created by {creator} on {created}; {expires} ({reason})" +msgstr "" + +msgid "{number}: sent by {sender} at {date/time}" +msgstr "" + +msgid "{number}: {command} on {service}: {method}" +msgstr "" + +msgid "{number}: {message}" +msgstr "" + +msgid "{number}: {message} -- created by {creator} at {created}" +msgstr "" + +msgid "{number}: {text} -- created by {creator} on {created}" +msgstr "" diff --git a/modules/botserv/bs_badwords.cpp b/modules/botserv/bs_badwords.cpp index f1b5a2ba3..fb9cebea5 100644 --- a/modules/botserv/bs_badwords.cpp +++ b/modules/botserv/bs_badwords.cpp @@ -246,11 +246,12 @@ private: { bool override = !source.AccessFor(ci).HasPriv("BADWORDS"); Log(override ? LOG_OVERRIDE : LOG_COMMAND, source, this, ci) << "LIST"; + ListFormatter list(source.GetAccount()); - BadWords *bw = ci->GetExt("badwords"); - list.AddColumn(_("Number")).AddColumn(_("Word")).AddColumn(_("Type")); + list.SetFlexible(_("{number}: \002{word}\002 -- type: {type}")); + BadWords *bw = ci->GetExt("badwords"); if (!bw || !bw->GetBadWordCount()) { source.Reply(_("%s bad words list is empty."), ci->name.c_str()); diff --git a/modules/botserv/bs_botlist.cpp b/modules/botserv/bs_botlist.cpp index fa17e6c01..2f89c6e79 100644 --- a/modules/botserv/bs_botlist.cpp +++ b/modules/botserv/bs_botlist.cpp @@ -41,11 +41,11 @@ public: } } - unsigned count = 0; ListFormatter list(source.GetAccount()); - list.AddColumn(_("Nick")).AddColumn(_("Mask")).AddColumn(_("Real name")); + list.SetFlexible(_("\002{nick}\002 ({mask}) [{real_name}]")); + unsigned count = 0; for (const auto &[_, bi] : *BotListByNick) { if (is_admin || !bi->oper_only) diff --git a/modules/chanserv/cs_access.cpp b/modules/chanserv/cs_access.cpp index e1881212b..ed81d4111 100644 --- a/modules/chanserv/cs_access.cpp +++ b/modules/chanserv/cs_access.cpp @@ -109,7 +109,7 @@ private: entry["Number"] = Anope::ToString(number); entry["Level"] = access->AccessSerialize(); entry["Mask"] = access->Mask(); - entry["By"] = access->creator; + entry["Creator"] = access->creator; entry["Last seen"] = timebuf; entry["Description"] = access->description; list.AddEntry(entry); @@ -453,6 +453,13 @@ private: ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Level")).AddColumn(_("Mask")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("{number}: \002{mask}\002 = {level}") + : _("{number}: \002{mask}\002 = {level} ({description})"); + }); + this->ProcessList(source, ci, params, list); } @@ -465,7 +472,14 @@ private: } ListFormatter list(source.GetAccount()); - list.AddColumn(_("Number")).AddColumn(_("Level")).AddColumn(_("Mask")).AddColumn(_("By")).AddColumn(_("Last seen")).AddColumn(_("Description")); + list.AddColumn(_("Number")).AddColumn(_("Level")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Last seen")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("{number}: \002{mask}\002 = {level} -- created by {creator}; last seen {last_seen}") + : _("{number}: \002{mask}\002 = {level} -- created by {creator}; last seen {last_seen} ({description})"); + }); + this->ProcessList(source, ci, params, list); } @@ -718,6 +732,7 @@ class CommandCSLevels final ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Level")); + list.SetFlexible(_("\002{name}\002 = {level}")); const std::vector &privs = PrivilegeManager::GetPrivileges(); @@ -815,6 +830,7 @@ public: ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Description")); + list.SetFlexible(_("\002{name}\002: {description}")); for (const auto &p : PrivilegeManager::GetPrivileges()) { diff --git a/modules/chanserv/cs_akick.cpp b/modules/chanserv/cs_akick.cpp index e5c7ba631..077c2cd08 100644 --- a/modules/chanserv/cs_akick.cpp +++ b/modules/chanserv/cs_akick.cpp @@ -394,6 +394,13 @@ class CommandCSAKick final ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Reason")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Reason"].empty() + ? _("{number}: \002{mask}\002") + : _("{number}: \002{mask}\002 ({reason})"); + }); + this->ProcessList(source, ci, params, list); } @@ -407,6 +414,13 @@ class CommandCSAKick final ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Last used")).AddColumn(_("Reason")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Reason"].empty() + ? _("{number}: \002{mask}\002 -- added by {creator} on {created}; last used: {last_used}") + : _("{number}: \002{mask}\002 -- added by {creator} on {created}; last used: {last_used} ({reason})"); + }); + this->ProcessList(source, ci, params, list); } diff --git a/modules/chanserv/cs_entrymsg.cpp b/modules/chanserv/cs_entrymsg.cpp index 6c8bbae10..a560a1564 100644 --- a/modules/chanserv/cs_entrymsg.cpp +++ b/modules/chanserv/cs_entrymsg.cpp @@ -128,6 +128,8 @@ private: ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Message")); + list.SetFlexible(_("{number}: {message} -- created by {creator} at {created}")); + for (unsigned i = 0; i < (*messages)->size(); ++i) { EntryMsg *msg = (*messages)->at(i); diff --git a/modules/chanserv/cs_flags.cpp b/modules/chanserv/cs_flags.cpp index 337e6e463..bfd8433c4 100644 --- a/modules/chanserv/cs_flags.cpp +++ b/modules/chanserv/cs_flags.cpp @@ -309,8 +309,13 @@ class CommandCSFlags final } ListFormatter list(source.GetAccount()); - list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Flags")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("{number}: \002{mask}\002 = {flags} -- added by {creator} at {created}") + : _("{number}: \002{mask}\002 = {flags} -- added by {creator} at {created} ({description})"); + }); unsigned count = 0; for (unsigned i = 0, end = ci->GetAccessCount(); i < end; ++i) diff --git a/modules/chanserv/cs_list.cpp b/modules/chanserv/cs_list.cpp index 3d1404a01..c2aa0d60d 100644 --- a/modules/chanserv/cs_list.cpp +++ b/modules/chanserv/cs_list.cpp @@ -76,6 +76,12 @@ public: ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("\002{name}\002") + : _("\002{name}\002 ({description})"); + }); Anope::map ordered_map; for (const auto &[cname, ci] : *RegisteredChannelList) diff --git a/modules/chanserv/cs_log.cpp b/modules/chanserv/cs_log.cpp index 752001efb..a2200382f 100644 --- a/modules/chanserv/cs_log.cpp +++ b/modules/chanserv/cs_log.cpp @@ -139,6 +139,7 @@ public: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Service")).AddColumn(_("Command")).AddColumn(_("Method")); + list.SetFlexible(_("{number}: {command} on {service}: {method}")); for (unsigned i = 0; i < (*ls)->size(); ++i) { diff --git a/modules/chanserv/cs_mode.cpp b/modules/chanserv/cs_mode.cpp index 96adfa7fd..5af811bf8 100644 --- a/modules/chanserv/cs_mode.cpp +++ b/modules/chanserv/cs_mode.cpp @@ -450,6 +450,12 @@ class CommandCSMode final { ListFormatter list(source.GetAccount()); list.AddColumn(_("Mode")).AddColumn(_("Param")).AddColumn(_("Creator")).AddColumn(_("Created")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Param"].empty() + ? _("{mode} -- created by {creator} on {created}") + : _("{mode} {param} -- created by {creator} on {created}"); + }); for (auto *ml : mlocks) { diff --git a/modules/chanserv/cs_xop.cpp b/modules/chanserv/cs_xop.cpp index 5f5b8fe03..124e36829 100644 --- a/modules/chanserv/cs_xop.cpp +++ b/modules/chanserv/cs_xop.cpp @@ -397,6 +397,12 @@ private: ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("{number}: \002{mask}\002") + : _("{number}: \002{mask}\002 ({description})"); + }); if (!nick.empty() && nick.find_first_not_of("1234567890,-") == Anope::string::npos) { diff --git a/modules/global/gl_queue.cpp b/modules/global/gl_queue.cpp index 90cb2579d..28d8be141 100644 --- a/modules/global/gl_queue.cpp +++ b/modules/global/gl_queue.cpp @@ -114,6 +114,8 @@ private: ListFormatter list(source.nc); list.AddColumn(_("Number")).AddColumn(_("Message")); + list.SetFlexible(_("{number}: {message}")); + for (size_t i = 0; i < q->size(); ++i) { ListFormatter::ListEntry entry; diff --git a/modules/hostserv/hs_list.cpp b/modules/hostserv/hs_list.cpp index 025f0d951..abc40d69d 100644 --- a/modules/hostserv/hs_list.cpp +++ b/modules/hostserv/hs_list.cpp @@ -52,8 +52,10 @@ public: } unsigned display_counter = 0, listmax = Config->GetModule(this->owner).Get("listmax", "50"); + ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Nick")).AddColumn(_("VHost")).AddColumn(_("Creator")).AddColumn(_("Created")); + list.SetFlexible(_("{number}: \002{nick}\002 = {vhost} -- created by {creator} at {created}")); for (const auto &[_, na] : *NickAliasList) { diff --git a/modules/hostserv/hs_request.cpp b/modules/hostserv/hs_request.cpp index 39e58b3bb..78b8d9fc1 100644 --- a/modules/hostserv/hs_request.cpp +++ b/modules/hostserv/hs_request.cpp @@ -441,9 +441,10 @@ public: { unsigned counter = 0; unsigned display_counter = 0, listmax = Config->GetModule(this->owner).Get("listmax"); - ListFormatter list(source.GetAccount()); + ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Nick")).AddColumn(_("VHost")).AddColumn(_("Created")); + list.SetFlexible(_("{number}: \002{nick}\002 = {vhost} -- created by {creator} at {created}")); for (const auto &[nick, na] : *NickAliasList) { diff --git a/modules/memoserv/ms_ignore.cpp b/modules/memoserv/ms_ignore.cpp index 056c2571a..f81aca9d3 100644 --- a/modules/memoserv/ms_ignore.cpp +++ b/modules/memoserv/ms_ignore.cpp @@ -77,6 +77,8 @@ public: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Mask")); + list.SetFlexible(_("\002{mask}\002")); + for (const auto &ignore : mi->ignores) { ListFormatter::ListEntry entry; diff --git a/modules/memoserv/ms_list.cpp b/modules/memoserv/ms_list.cpp index 5d867e1b6..619c84879 100644 --- a/modules/memoserv/ms_list.cpp +++ b/modules/memoserv/ms_list.cpp @@ -61,8 +61,8 @@ public: else { ListFormatter list(source.GetAccount()); - list.AddColumn(_("Number")).AddColumn(_("Sender")).AddColumn(_("Date/Time")); + list.SetFlexible(_("{number}: sent by \002{sender}\002 at {date/time}")); if (!param.empty() && isdigit(param[0])) { diff --git a/modules/nickserv/ns_ajoin.cpp b/modules/nickserv/ns_ajoin.cpp index cbe5cbba0..e940cb6be 100644 --- a/modules/nickserv/ns_ajoin.cpp +++ b/modules/nickserv/ns_ajoin.cpp @@ -113,6 +113,13 @@ class CommandNSAJoin final { ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Channel")).AddColumn(_("Key")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Key"].empty() + ? _("{number}: \002{channel}\002") + : _("{number}: \002{channel}\002 (key: {key})"); + }); + for (unsigned i = 0; i < (*channels)->size(); ++i) { AJoinEntry *aj = (*channels)->at(i); diff --git a/modules/nickserv/ns_alist.cpp b/modules/nickserv/ns_alist.cpp index f62348721..51c7814df 100644 --- a/modules/nickserv/ns_alist.cpp +++ b/modules/nickserv/ns_alist.cpp @@ -38,10 +38,16 @@ public: nc = na->nc; } - ListFormatter list(source.GetAccount()); int chan_count = 0; + ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Channel")).AddColumn(_("Access")).AddColumn(_("Description")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("{number}: \002{channel}\002 = {access}") + : _("{number}: \002{channel}\002 = {access} ({description})"); + }); std::deque queue; nc->GetChannelReferences(queue); diff --git a/modules/nickserv/ns_group.cpp b/modules/nickserv/ns_group.cpp index d9fcba709..bfd081c9d 100644 --- a/modules/nickserv/ns_group.cpp +++ b/modules/nickserv/ns_group.cpp @@ -333,6 +333,13 @@ public: ListFormatter list(source.GetAccount()); list.AddColumn(_("Nick")).AddColumn(_("Expires")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Expires"].equals_cs(NO_EXPIRE) + ? _("\002{nick}\002") + : _("\002{nick}\002: expires in {expires}"); + }); + time_t nickserv_expire = Config->GetModule("nickserv").Get("expire", "90d"), unconfirmed_expire = Config->GetModule("ns_register").Get("unconfirmedexpire", "1d"); for (auto *na2 : *nc->aliases) diff --git a/modules/nickserv/ns_list.cpp b/modules/nickserv/ns_list.cpp index 50fd18fb5..106ada32e 100644 --- a/modules/nickserv/ns_list.cpp +++ b/modules/nickserv/ns_list.cpp @@ -71,9 +71,15 @@ public: } mync = source.nc; - ListFormatter list(source.GetAccount()); + ListFormatter list(source.GetAccount()); list.AddColumn(_("Nick")).AddColumn(_("Last mask")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Last mask"].empty() + ? _("\002{nick}\002") + : _("\002{nick}\002 (last mask: {last_mask})"); + }); Anope::map ordered_map; for (const auto &[nick, na] : *NickAliasList) diff --git a/modules/nickserv/ns_set_layout.cpp b/modules/nickserv/ns_set_layout.cpp new file mode 100644 index 000000000..de3416c4e --- /dev/null +++ b/modules/nickserv/ns_set_layout.cpp @@ -0,0 +1,145 @@ +/* NickServ core functions + * + * (C) 2003-2025 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" + +class CommandNSSetLayout + : public Command +{ +public: + CommandNSSetLayout(Module *creator, const Anope::string &sname = "nickserv/set/layout", size_t min = 1) + : Command(creator, sname, min, min + 1) + { + this->SetDesc(_("Configures the layout used for services messages")); + this->SetSyntax("{FIXED | FLEXIBLE}"); + } + + void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m) + { + if (Anope::ReadOnly) + { + source.Reply(READ_ONLY_MODE); + return; + } + + const auto *na = NickAlias::Find(user); + if (!na) + { + source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); + return; + } + NickCore *nc = na->nc; + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, nc, param)); + if (MOD_RESULT == EVENT_STOP) + return; + + if (param.equals_ci("FIXED")) + { + nc->Shrink("NS_FLEXIBLE"); + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to change the layout to fixed for " << nc->display; + source.Reply(_("Layout is now \002fixed\002 for \002%s\002."), nc->display.c_str()); + } + else if (param.equals_ci("FLEXIBLE")) + { + nc->Extend("NS_FLEXIBLE"); + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to change the layout to flexible for " << nc->display; + source.Reply(_("Layout is now \002flexible\002 for \002%s\002."), nc->display.c_str()); + } + else + { + this->OnSyntaxError(source, "LAYOUT"); + } + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + this->Run(source, source.nc->display, params[0]); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_( + "Configures the layout used for services messages. When the layout is set to " + "\037FIXED\037 services will use tables and position text such that it looks " + "good in a client that uses a fixed-width font. When the layout is set to " + "\037FLEXIBLE\037 services will use an alternate format for messages and avoid " + "any positioning that might be broken by a variable-width font." + )); + return true; + } +}; + +class CommandNSSASetLayout final + : public CommandNSSetLayout +{ +public: + CommandNSSASetLayout(Module *creator) + : CommandNSSetLayout(creator, "nickserv/saset/layout", 2) + { + this->ClearSyntax(); + this->SetSyntax(_("\037nickname\037 {FIXED | FLEXIBLE}")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + this->Run(source, params[0], params[1]); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_( + "Configures the layout used by the account for services messages. When the " + "layout is set to \037FIXED\037 services will use tables and position text such " + "that it looks good in a client that uses a fixed-width font. When the layout is " + "set to \037FLEXIBLE\037 services will use an alternate format for messages and " + "avoid any positioning that might be broken by a variable-width font." + )); + return true; + } +}; + +class NSSetLayout final + : public Module +{ +private: + CommandNSSetLayout commandnssetlayout; + CommandNSSASetLayout commandnssasetlayout; + + SerializableExtensibleItem nsflexible; + +public: + NSSetLayout(const Anope::string &modname, const Anope::string &creator) + : Module(modname, creator, VENDOR) + , commandnssetlayout(this) + , commandnssasetlayout(this) + , nsflexible(this, "NS_FLEXIBLE") + { + } + + void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) override + { + if (nsflexible.HasExt(na->nc)) + info.AddOption(_("Flexible layout")); + else + info.AddOption(_("Fixed layout")); + + if (!show_hidden) + return; + } +}; + +MODULE_INIT(NSSetLayout) diff --git a/modules/operserv/os_akill.cpp b/modules/operserv/os_akill.cpp index a70b03cbe..06c21a9f8 100644 --- a/modules/operserv/os_akill.cpp +++ b/modules/operserv/os_akill.cpp @@ -352,6 +352,7 @@ private: ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Reason")); + list.SetFlexible(_("{number}: \002{mask}\002 ({reason})")); this->ProcessList(source, params, list); } @@ -364,11 +365,19 @@ private: return; } + const auto akillids = Config->GetModule("operserv").Get("akillids"); + ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Expires")); - if (Config->GetModule("operserv").Get("akillids")) + if (akillids) list.AddColumn(_("ID")); list.AddColumn(_("Reason")); + list.SetFlexible([akillids](ListFormatter::ListEntry &row) + { + return akillids + ? _("{number}: [{id}] \002{mask}\002 -- created by {creator} on {created}; {expires} ({reason})") + : _("{number}: \002{mask}\002 -- created by {creator} on {created}; {expires} ({reason})"); + }); this->ProcessList(source, params, list); } diff --git a/modules/operserv/os_config.cpp b/modules/operserv/os_config.cpp index 827df6c32..3a3ae5c32 100644 --- a/modules/operserv/os_config.cpp +++ b/modules/operserv/os_config.cpp @@ -62,6 +62,7 @@ public: ListFormatter lflist(source.GetAccount()); lflist.AddColumn(_("Name")).AddColumn(_("Value")); + lflist.SetFlexible(_("\002{name}\002 = {value}")); for (const auto &[name, value] : items) { @@ -78,6 +79,7 @@ public: ListFormatter lflist(source.GetAccount()); lflist.AddColumn(_("Module Name")).AddColumn(_("Name")).AddColumn(_("Value")); + lflist.SetFlexible(_("\002{}{module_name}}:{name}\002 = {value}")); for (int i = 0; i < Config->CountBlock("module"); ++i) { diff --git a/modules/operserv/os_dns.cpp b/modules/operserv/os_dns.cpp index f69da370b..b114d09a1 100644 --- a/modules/operserv/os_dns.cpp +++ b/modules/operserv/os_dns.cpp @@ -241,6 +241,8 @@ class CommandOSDNS final ListFormatter lf(source.GetAccount()); lf.AddColumn(_("Server")).AddColumn(_("IP")).AddColumn(_("Limit")).AddColumn(_("State")); + lf.SetFlexible(_("Server: \002{server}\002 = {ip} -- limit: {limit}; state: {state}")); + for (auto *s : *dns_servers) { Server *srv = Server::Find(s->GetName(), true); @@ -275,6 +277,12 @@ class CommandOSDNS final { ListFormatter lf2(source.GetAccount()); lf2.AddColumn(_("Zone")).AddColumn(_("Servers")); + lf2.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Servers"].equals_cs("None") + ? _("Zone: \002{zone}\002") + : _("Zone: \002{zone}\002 = {servers}"); + }); for (auto *z : *zones) { diff --git a/modules/operserv/os_forbid.cpp b/modules/operserv/os_forbid.cpp index 6f8c6576e..c2729bb73 100644 --- a/modules/operserv/os_forbid.cpp +++ b/modules/operserv/os_forbid.cpp @@ -402,6 +402,12 @@ public: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Mask")).AddColumn(_("Type")).AddColumn(_("Creator")).AddColumn(_("Expires")).AddColumn(_("Reason")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Reason"].empty() + ? _("\002{mask}\002 on {type} -- created by {creator}; expires in {expires}") + : _("\002{mask}\002 on {type} -- created by {creator}; expires in {expires} ({reason})"); + }); size_t shown = 0; for (auto *forbid : forbids) diff --git a/modules/operserv/os_ignore.cpp b/modules/operserv/os_ignore.cpp index caae71c6b..1bec9a1c9 100644 --- a/modules/operserv/os_ignore.cpp +++ b/modules/operserv/os_ignore.cpp @@ -278,6 +278,12 @@ private: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Reason")).AddColumn(_("Expires")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Reason"].empty() + ? _("\002{mask}\002 -- created by {creator}; {expires}") + : _("\002{mask}\002 -- created by {creator}; {expires} ({reason})"); + }); for (unsigned i = ignores.size(); i > 0; --i) { diff --git a/modules/operserv/os_list.cpp b/modules/operserv/os_list.cpp index 60cb9fbad..db1995f0a 100644 --- a/modules/operserv/os_list.cpp +++ b/modules/operserv/os_list.cpp @@ -42,6 +42,13 @@ public: ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Users")).AddColumn(_("Modes")).AddColumn(_("Topic")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Topic"].empty() + ? _("\002{name}\002 -- {users} user(s); +{modes}") + : _("\002{name}\002 -- {users} user(s); +{modes} ({topic})"); + }); + if (!pattern.empty() && (u2 = User::Find(pattern, true))) { @@ -157,6 +164,7 @@ public: ListFormatter list(source.GetAccount()); list.AddColumn(_("Name")).AddColumn(_("Mask")).AddColumn(_("Realname")); + list.SetFlexible(_("\002{name}\002 ({mask}) [{realname}]")); if (!pattern.empty() && (c = Channel::Find(pattern))) { diff --git a/modules/operserv/os_news.cpp b/modules/operserv/os_news.cpp index 859ebd7d6..9badf188c 100644 --- a/modules/operserv/os_news.cpp +++ b/modules/operserv/os_news.cpp @@ -205,6 +205,7 @@ protected: { ListFormatter lflist(source.GetAccount()); lflist.AddColumn(_("Number")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Text")); + lflist.SetFlexible(_("{number}: {text} -- created by {creator} on {created}")); for (unsigned i = 0, end = list.size(); i < end; ++i) { diff --git a/modules/operserv/os_session.cpp b/modules/operserv/os_session.cpp index c4cb98485..c5be0f912 100644 --- a/modules/operserv/os_session.cpp +++ b/modules/operserv/os_session.cpp @@ -236,6 +236,7 @@ private: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Session")).AddColumn(_("Host")); + list.SetFlexible(_("\002{host}\002: {session} sessions")); for (const auto &[_, session] : session_service->GetSessions()) { @@ -492,7 +493,7 @@ private: ListFormatter::ListEntry entry; entry["Number"] = Anope::ToString(Number); entry["Mask"] = e->mask; - entry["By"] = e->who; + entry["Creator"] = e->who; entry["Created"] = Anope::strftime(e->time, NULL, true); entry["Expires"] = Anope::Expires(e->expires, source.GetAccount()); entry["Limit"] = Anope::ToString(e->limit); @@ -513,7 +514,7 @@ private: ListFormatter::ListEntry entry; entry["Number"] = Anope::ToString(i + 1); entry["Mask"] = e->mask; - entry["By"] = e->who; + entry["Creator"] = e->who; entry["Created"] = Anope::strftime(e->time, NULL, true); entry["Expires"] = Anope::Expires(e->expires, source.GetAccount()); entry["Limit"] = Anope::ToString(e->limit); @@ -536,6 +537,7 @@ private: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Limit")).AddColumn(_("Mask")); + list.SetFlexible(_("{number}: \002{mask}\002 -- {limit} sessions")); this->ProcessList(source, params, list); } @@ -543,7 +545,8 @@ private: void DoView(CommandSource &source, const std::vector ¶ms) { ListFormatter list(source.GetAccount()); - list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("By")).AddColumn(_("Created")).AddColumn(_("Expires")).AddColumn(_("Limit")).AddColumn(_("Reason")); + list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Expires")).AddColumn(_("Limit")).AddColumn(_("Reason")); + list.SetFlexible(_("{number}: \002{mask}\002 -- {limit} sessions; created by {creator} on {created}; {expires} ({reason})")); this->ProcessList(source, params, list); } diff --git a/modules/operserv/os_sxline.cpp b/modules/operserv/os_sxline.cpp index d6ceac347..c9e06bac6 100644 --- a/modules/operserv/os_sxline.cpp +++ b/modules/operserv/os_sxline.cpp @@ -154,7 +154,7 @@ private: ListFormatter::ListEntry entry; entry["Number"] = Anope::ToString(number); entry["Mask"] = x->mask; - entry["By"] = x->by; + entry["Creator"] = x->by; entry["Created"] = Anope::strftime(x->created, NULL, true); entry["Expires"] = Anope::Expires(x->expires, source.nc); entry["ID"] = x->id; @@ -176,7 +176,7 @@ private: ListFormatter::ListEntry entry; entry["Number"] = Anope::ToString(i + 1); entry["Mask"] = x->mask; - entry["By"] = x->by; + entry["Creator"] = x->by; entry["Created"] = Anope::strftime(x->created, NULL, true); entry["Expires"] = Anope::Expires(x->expires, source.nc); entry["ID"] = x->id; @@ -199,17 +199,27 @@ private: { ListFormatter list(source.GetAccount()); list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Reason")); + list.SetFlexible(_("{number}: \002{mask}\002 ({reason})")); this->ProcessList(source, params, list); } void OnView(CommandSource &source, const std::vector ¶ms) { + const auto akillids = Config->GetModule("operserv").Get("akillids"); + ListFormatter list(source.GetAccount()); - list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("By")).AddColumn(_("Created")).AddColumn(_("Expires")); + list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Expires")); if (Config->GetModule("operserv").Get("akillids")) list.AddColumn(_("ID")); list.AddColumn(_("Reason")); + list.SetFlexible([akillids](ListFormatter::ListEntry &row) + { + return akillids + ? _("{number}: [{id}] \002{mask}\002 -- created by {creator} on {created}; {expires} ({reason})") + : _("{number}: \002{mask}\002 -- created by {creator} on {created}; {expires} ({reason})"); + }); + this->ProcessList(source, params, list); } diff --git a/src/command.cpp b/src/command.cpp index 475477dd6..d45299c6f 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -148,6 +148,9 @@ void Command::SetSyntax(const Anope::string &s, const std::functionHasExt("NS_FLEXIBLE") : false; + auto first = true; Anope::string prefix = Language::Translate(source.GetAccount(), _("Syntax")); Anope::string padding(prefix.utf8length(), ' '); @@ -156,21 +159,21 @@ void Command::SendSyntax(CommandSource &source) if (predicate && !predicate(source)) continue; // Not for this user. - if (first) + if (first || flexible) { first = false; - source.Reply("%s: \002%s %s\002", prefix.c_str(), source.command.nobreak().c_str(), + source.Reply(_("%s: \002%s %s\002"), prefix.c_str(), source.command.nobreak().c_str(), Language::Translate(source.GetAccount(), syntax.c_str())); } else { - source.Reply("%s \002%s %s\002", padding.c_str(), source.command.nobreak().c_str(), + source.Reply(_("%s \002%s %s\002"), padding.c_str(), source.command.nobreak().c_str(), Language::Translate(source.GetAccount(), syntax.c_str())); } } if (first) - source.Reply("%s: \002%s\002", prefix.c_str(), source.command.nobreak().c_str()); + source.Reply(_("%s: \002%s\002"), prefix.c_str(), source.command.nobreak().c_str()); } void Command::AllowUnregistered(bool b) diff --git a/src/misc.cpp b/src/misc.cpp index 8398fffca..58a55344f 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -142,6 +142,15 @@ bool ListFormatter::IsEmpty() const } void ListFormatter::SendTo(CommandSource &source) +{ + const auto *sourcenc = flexiblerow ? source.GetAccount() : nullptr; + if (sourcenc ? sourcenc->HasExt("NS_FLEXIBLE") : false) + SendFlexible(source); + else + SendFixed(source); +} + +void ListFormatter::SendFixed(CommandSource &source) { std::vector tcolumns; std::map lengths; @@ -215,19 +224,55 @@ void ListFormatter::SendTo(CommandSource &source) } } +void ListFormatter::SendFlexible(CommandSource &source) +{ + for (auto &entry : entries) + { + // Build a map that we can template from. + Anope::map variables; + for (const auto &[ekey, evalue] : entry) + { + const auto tkey = ekey.lower().replace_all_cs(" ", "_"); + variables[tkey] = evalue; + } + + const auto row = this->flexiblerow(entry); + const auto *translated_row = Language::Translate(this->nc, row.c_str()); + source.Reply(Anope::Template(translated_row, variables)); + } +} + +void ListFormatter::SetFlexible(const Anope::string &format) +{ + this->flexiblerow = [format](const ListEntry &) { return format; }; +} + +void ListFormatter::SetFlexible(const FlexibleFormatFn &formatter) +{ + this->flexiblerow = formatter; +} + InfoFormatter::InfoFormatter(NickCore *acc) : nc(acc) { } void InfoFormatter::SendTo(CommandSource &source) { + const auto *sourcenc = source.GetAccount(); + const auto flexible = sourcenc ? sourcenc->HasExt("NS_FLEXIBLE") : false; for (const auto &[key, value] : this->replies) { - auto line = key; - line += ": "; - line += Anope::string(longest - key.utf8length(), ' '); - line += Language::Translate(this->nc, value.c_str()); - source.Reply(line); + if (flexible) + { + source.Reply("\002%s\002: %s", key.c_str(), + Language::Translate(this->nc, value.c_str())); + } + else + { + Anope::string padding(longest - key.utf8length(), ' '); + source.Reply("%s: %s%s", key.c_str(), padding.c_str(), + Language::Translate(this->nc, value.c_str())); + } } } @@ -270,17 +315,28 @@ void HelpWrapper::AddEntry(const Anope::string &name, const Anope::string &desc) void HelpWrapper::SendTo(CommandSource &source) { + const auto *sourcenc = source.GetAccount(); + const auto flexible = sourcenc ? sourcenc->HasExt("NS_FLEXIBLE") : false; + const auto max_length = Config->GetBlock("options").Get("linelength", "100") - longest - 8; + for (const auto &[entry_name, entry_desc] : entries) { - LineWrapper lw(Language::Translate(source.nc, entry_desc.c_str()), max_length); + if (flexible) + { + source.Reply("\002%s\002: %s", entry_name.c_str(), entry_desc.c_str()); + } + else + { + LineWrapper lw(Language::Translate(source.nc, entry_desc.c_str()), max_length); - Anope::string line; - if (lw.GetLine(line)) - source.Reply(" %-*s %s", (int)longest, entry_name.c_str(), line.c_str()); + Anope::string line; + if (lw.GetLine(line)) + source.Reply(" %-*s %s", (int)longest, entry_name.c_str(), line.c_str()); - while (lw.GetLine(line)) - source.Reply(" %-*s %s", (int)longest, "", line.c_str()); + while (lw.GetLine(line)) + source.Reply(" %-*s %s", (int)longest, "", line.c_str()); + } } };