diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 3dfe6ce982..59d3f7fb07 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -14,6 +14,7 @@ from evennia.utils.utils import fill, dedent from evennia.commands.command import Command from evennia.help.models import HelpEntry from evennia.utils import create, evmore +from evennia.utils.ansi import ANSIString from evennia.utils.eveditor import EvEditor from evennia.utils.utils import string_suggestions, class_from_module, inherits_from, format_grid @@ -40,6 +41,7 @@ class HelpCategory: "tags": "", "text": "" } + def __str__(self): return f"Category: {self.key}" @@ -178,39 +180,27 @@ class CmdHelp(Command): string += "\n" + _SEP return string - @staticmethod - def format_help_list(hdict_cmds, hdict_db): + def format_help_list(self, hdict_cmds, hdict_db): """ Output a category-ordered list. The input are the pre-loaded help files for commands and database-helpfiles respectively. You can override this method to return a custom display of the list of commands and topics. """ - output = [] + width = self.client_width() + grid = [] + verbatim_elements = [] for category in sorted(set(list(hdict_cmds.keys()) + list(hdict_db.keys()))): - output.append(f"|w{category.title()}|G") + + category_str = f"-- {category.title()} " + grid.append(ANSIString("|w" + category_str + "-" * (width - len(category_str)) + "|G")) + verbatim_elements.append(len(grid) - 1) + entries = sorted(set(hdict_cmds.get(category, []) + hdict_db.get(category, []))) - output.append(format_grid(entries, width=78)) # self.client_width())) - return "\n".join(output) + grid.extend(entries) - string = "" - if hdict_cmds and any(hdict_cmds.values()): - string += "\n" + _SEP + "\n |CCommand help entries|n\n" + _SEP - for category in sorted(hdict_cmds.keys()): - string += "\n |w%s|n:\n" % (str(category).title()) - string += "|G" + fill("|C, |G".join(sorted(hdict_cmds[category]))) + "|n" - - if hdict_db and any(hdict_db.values()): - string += "\n\n" + _SEP + "\n\r |COther help entries|n\n" + _SEP - for category in sorted(hdict_db.keys()): - string += "\n\r |w%s|n:\n" % (str(category).title()) - string += ( - "|G" - + fill(", ".join(sorted([str(topic) for topic in hdict_db[category]]))) - + "|n" - ) - - return string + gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements) + return "\n".join(gridrows) def check_show_help(self, cmd, caller): """ @@ -320,9 +310,9 @@ class CmdHelp(Command): if isinstance(match, HelpCategory): formatted = self.format_help_list( {match.key: [cmd.key for cmd in all_cmds - if match.key.lower() == cmd.help_category]}, + if match.key.lower() == cmd.help_category]}, {match.key: [topic.key for topic in all_topics - if match.key.lower() == topic.help_category]} + if match.key.lower() == topic.help_category]} ) elif inherits_from(match, "evennia.commands.command.Command"): formatted = self.format_help_entry( diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index 1e0e6e93aa..fea698740b 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -298,7 +298,7 @@ class EvMore(object): nsize = len(inp) self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1) self._data = inp - self._paginator_slice + self._paginator = self.paginator_slice def init_f_str(self, text): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 2571f3ec01..b16ac6511c 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -289,8 +289,6 @@ class TestFormatGrid(TestCase): elements = self._generate_elements(3, 1, 30) result = utils.format_grid(elements, width=78) rows = result.split("\n") - for row in rows: - print(f"'{row}'") self.assertEqual(len(rows), 3) self.assertTrue(all(len(row) == 78 for row in rows)) @@ -299,20 +297,14 @@ class TestFormatGrid(TestCase): elements = self._generate_elements(3, 15, 30) result = utils.format_grid(elements, width=82, sep=" ") rows = result.split("\n") - for row in rows: - print(f"'{row}'") self.assertEqual(len(rows), 8) self.assertTrue(all(len(row) == 82 for row in rows)) def test_huge_grid(self): """Grid with very long strings""" - # from pudb import debugger - # debugger.Debugger().set_trace() elements = self._generate_elements(70, 20, 30) result = utils.format_grid(elements, width=78) rows = result.split("\n") - for row in rows: - print(f"'{row}'") self.assertEqual(len(rows), 30) self.assertTrue(all(len(row) == 78 for row in rows)) @@ -324,7 +316,18 @@ class TestFormatGrid(TestCase): result = utils.format_grid(elements, width=78) rows = result.split("\n") self.assertEqual(len(rows), 2) - for row in rows: - print(f"'{row}'") for element in elements: self.assertTrue(element in result, f"element {element} is missing.") + + def test_breakline(self): + """Grid with line-long elements in middle""" + elements = self._generate_elements(6, 4, 30) + elements[10] = elements[20] = "-" * 78 + # from pudb import debugger + # debugger.Debugger().set_trace() + result = utils.format_grid(elements, width=78) + rows = result.split("\n") + self.assertEqual(len(rows), 8) + for element in elements: + self.assertTrue(element in result, f"element {element} is missing.") + diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index cba9423cd0..47589c42dd 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -153,6 +153,7 @@ def crop(text, width=None, suffix="[...]"): if ltext <= width: return text else: + from evennia import set_trace;set_trace() lsuffix = len(suffix) text = text[:width] if lsuffix >= width else "%s%s" % (text[: width - lsuffix], suffix) return to_str(text) @@ -1713,7 +1714,7 @@ def percentile(iterable, percent, key=lambda x: x): return d0 + d1 -def format_grid(elements, width=78, sep=" "): +def format_grid(elements, width=78, sep=" ", verbatim_elements=None): """ This helper function makes a 'grid' output, where it distributes the given string-elements as evenly as possible to fill out the given width. @@ -1723,52 +1724,63 @@ def format_grid(elements, width=78, sep=" "): elements (iterable): A 1D list of string elements to put in the grid. width (int, optional): The width of the grid area to fill. sep (str, optional): The extra separator to put between words. If - set to the empty string, words may run into each other + set to the empty string, words may run into each other. + verbatim_elements (list, optional): This is a list of indices pointing to + specific items in the `elements` list. An element at this index will + not be included in the calculation of the slot sizes. It will still + be inserted into the grid at the correct position and may be surrounded + by padding unless filling the entire line. This is useful for embedding + decorations in the grid, such as horizontal bars. Returns: - gridstr (str): The grid as a finished renderede multi-line string. - + gridstr (list): The grid as a list of ready-formatted rows. We return it + like this to make it easier to insert decorations between rows, such + as horizontal bars. """ - nelements = len(elements) - elements = [elements[ie] + sep for ie in range(nelements - 1)] + [elements[-1]] + if not verbatim_elements: + verbatim_elements = [] + nelements = len(elements) + # add sep to all but the very last element + elements = [elements[ie] + sep for ie in range(nelements - 1)] + [elements[-1]] wls = [len(elem) for elem in elements] + wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements] + # get the nth percentile as a good representation of average width - averlen = int(percentile(sorted(wls), 0.9)) + 2 # include extra space + averlen = int(percentile(sorted(wls_percentile), 0.9)) + 2 # include extra space aver_per_row = width // averlen + 1 + if aver_per_row == 1: + # one line per row, output directly since this is trivial + # we use rstrip here to remove extra spaces added by sep + return "\n".join( + crop(element.rstrip(), width) + " " * max(0, width - len(element.rstrip())) + for iel, element in enumerate(elements) + ) + indices = [averlen * ind for ind in range(aver_per_row - 1)] rows = [] ic = 0 row = "" for ie, element in enumerate(elements): + wl = wls[ie] lrow = len(row) debug = row.replace(" ", ".") if lrow + wl > width: - # last slot extends outside grid, move to next line + # this slot extends outside grid, move to next line row += " " * (width - lrow) rows.append(row) + if wl >= width: + # remove sep if this fills the entire line + element = element.rstrip() row = crop(element, width) ic = 0 elif ic >= aver_per_row - 1: - # last slot on the line - if ic == 0: - # one slot per line - row = crop(element, width) - row += " " * max(0, (width - len(row))) - rows.append(row) - else: - # finish line, put slot on next line - row += " " * max(0, (width - lrow)) - rows.append(row) - row = crop(element, width) - ic = 0 - elif lrow + wl > width: - # last slot extends outside grid, move to next line - row += " " * (width - lrow) + # no more slots available on this line + row += " " * max(0, (width - lrow)) rows.append(row) row = crop(element, width) ic = 0 @@ -1793,9 +1805,10 @@ def format_grid(elements, width=78, sep=" "): if ie >= nelements - 1: # last element, make sure to store + row += " " * max(0, width - len(row)) rows.append(row) - return "\n".join(rows) + return rows def get_evennia_pids():