@@ -475,15 +475,22 @@ def __init__(
475475 # The multiline command currently being typed which is used to tab complete multiline commands.
476476 self ._multiline_in_progress = ''
477477
478- # Set the header used for the help function's listing of documented functions
479- self .doc_header = "Documented commands (use 'help -v' for verbose/'help <topic>' for details)"
478+ # Set text which prints right before all of the help topics are listed.
479+ self .doc_leader = ""
480+
481+ # Set header for table listing documented commands.
482+ self .doc_header = "Documented Commands"
480483
481484 # Set header for table listing help topics not related to a command.
482485 self .misc_header = "Miscellaneous Help Topics"
483486
484487 # Set header for table listing commands that have no help info.
485488 self .undoc_header = "Undocumented Commands"
486489
490+ # If any command has been categorized, then all other commands that haven't been categorized
491+ # will display under this section in the help output.
492+ self .default_category = "Uncategorized Commands"
493+
487494 # The error that prints when no help information can be found
488495 self .help_error = "No help on {}"
489496
@@ -551,10 +558,6 @@ def __init__(
551558 # values are DisabledCommand objects.
552559 self .disabled_commands : dict [str , DisabledCommand ] = {}
553560
554- # If any command has been categorized, then all other commands that haven't been categorized
555- # will display under this section in the help output.
556- self .default_category = 'Uncategorized'
557-
558561 # The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort.
559562 # If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
560563 # cmd2 uses this key for sorting:
@@ -4039,6 +4042,37 @@ def complete_help_subcommands(
40394042 completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
40404043 return completer .complete_subcommand_help (text , line , begidx , endidx , arg_tokens ['subcommands' ])
40414044
4045+ def _build_command_info (self ) -> tuple [dict [str , list [str ]], list [str ], list [str ], list [str ]]:
4046+ # Get a sorted list of help topics
4047+ help_topics = sorted (self .get_help_topics (), key = self .default_sort_key )
4048+
4049+ # Get a sorted list of visible command names
4050+ visible_commands = sorted (self .get_visible_commands (), key = self .default_sort_key )
4051+ cmds_doc : list [str ] = []
4052+ cmds_undoc : list [str ] = []
4053+ cmds_cats : dict [str , list [str ]] = {}
4054+ for command in visible_commands :
4055+ func = cast (CommandFunc , self .cmd_func (command ))
4056+ has_help_func = False
4057+ has_parser = func in self ._command_parsers
4058+
4059+ if command in help_topics :
4060+ # Prevent the command from showing as both a command and help topic in the output
4061+ help_topics .remove (command )
4062+
4063+ # Non-argparse commands can have help_functions for their documentation
4064+ has_help_func = not has_parser
4065+
4066+ if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
4067+ category : str = getattr (func , constants .CMD_ATTR_HELP_CATEGORY )
4068+ cmds_cats .setdefault (category , [])
4069+ cmds_cats [category ].append (command )
4070+ elif func .__doc__ or has_help_func or has_parser :
4071+ cmds_doc .append (command )
4072+ else :
4073+ cmds_undoc .append (command )
4074+ return cmds_cats , cmds_doc , cmds_undoc , help_topics
4075+
40424076 @classmethod
40434077 def _build_help_parser (cls ) -> Cmd2ArgumentParser :
40444078 help_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (
@@ -4074,7 +4108,24 @@ def do_help(self, args: argparse.Namespace) -> None:
40744108 self .last_result = True
40754109
40764110 if not args .command or args .verbose :
4077- self ._help_menu (args .verbose )
4111+ cmds_cats , cmds_doc , cmds_undoc , help_topics = self ._build_command_info ()
4112+
4113+ if self .doc_leader :
4114+ self .poutput ()
4115+ self .poutput (self .doc_leader , style = Cmd2Style .HELP_LEADER , soft_wrap = False )
4116+ self .poutput ()
4117+
4118+ if not cmds_cats :
4119+ # No categories found, fall back to standard behavior
4120+ self ._print_documented_command_topics (self .doc_header , cmds_doc , args .verbose )
4121+ else :
4122+ # Categories found, Organize all commands by category
4123+ for category in sorted (cmds_cats .keys (), key = self .default_sort_key ):
4124+ self ._print_documented_command_topics (category , cmds_cats [category ], args .verbose )
4125+ self ._print_documented_command_topics (self .default_category , cmds_doc , args .verbose )
4126+
4127+ self .print_topics (self .misc_header , help_topics , 15 , 80 )
4128+ self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
40784129
40794130 else :
40804131 # Getting help for a specific command
@@ -4113,128 +4164,23 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol:
41134164 """
41144165 if cmds :
41154166 header_grid = Table .grid ()
4116- header_grid .add_row (header , style = Cmd2Style .HELP_TITLE )
4167+ header_grid .add_row (header , style = Cmd2Style .HELP_HEADER )
41174168 if self .ruler :
41184169 header_grid .add_row (Rule (characters = self .ruler ))
41194170 self .poutput (header_grid )
41204171 self .columnize (cmds , maxcol - 1 )
41214172 self .poutput ()
41224173
4123- def columnize (self , str_list : list [str ] | None , display_width : int = 80 ) -> None :
4124- """Display a list of single-line strings as a compact set of columns.
4125-
4126- Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters.
4127-
4128- Each column is only as wide as necessary.
4129- Columns are separated by two spaces (one was not legible enough).
4130- """
4131- if not str_list :
4132- self .poutput ("<empty>" )
4133- return
4134-
4135- nonstrings = [i for i in range (len (str_list )) if not isinstance (str_list [i ], str )]
4136- if nonstrings :
4137- raise TypeError (f"str_list[i] not a string for i in { nonstrings } " )
4138- size = len (str_list )
4139- if size == 1 :
4140- self .poutput (str_list [0 ])
4141- return
4142- # Try every row count from 1 upwards
4143- for nrows in range (1 , len (str_list )):
4144- ncols = (size + nrows - 1 ) // nrows
4145- colwidths = []
4146- totwidth = - 2
4147- for col in range (ncols ):
4148- colwidth = 0
4149- for row in range (nrows ):
4150- i = row + nrows * col
4151- if i >= size :
4152- break
4153- x = str_list [i ]
4154- colwidth = max (colwidth , su .str_width (x ))
4155- colwidths .append (colwidth )
4156- totwidth += colwidth + 2
4157- if totwidth > display_width :
4158- break
4159- if totwidth <= display_width :
4160- break
4161- else :
4162- # The output is wider than display_width. Print 1 column with each string on its own row.
4163- nrows = len (str_list )
4164- ncols = 1
4165- colwidths = [1 ]
4166- for row in range (nrows ):
4167- texts = []
4168- for col in range (ncols ):
4169- i = row + nrows * col
4170- x = "" if i >= size else str_list [i ]
4171- texts .append (x )
4172- while texts and not texts [- 1 ]:
4173- del texts [- 1 ]
4174- for col in range (len (texts )):
4175- texts [col ] = su .align_left (texts [col ], width = colwidths [col ])
4176- self .poutput (" " .join (texts ))
4177-
4178- def _help_menu (self , verbose : bool = False ) -> None :
4179- """Show a list of commands which help can be displayed for."""
4180- cmds_cats , cmds_doc , cmds_undoc , help_topics = self ._build_command_info ()
4181-
4182- if not cmds_cats :
4183- # No categories found, fall back to standard behavior
4184- self .poutput (self .doc_leader , soft_wrap = False )
4185- self ._print_topics (self .doc_header , cmds_doc , verbose )
4186- else :
4187- # Categories found, Organize all commands by category
4188- self .poutput (self .doc_leader , style = Cmd2Style .HELP_HEADER , soft_wrap = False )
4189- self .poutput (self .doc_header , style = Cmd2Style .HELP_HEADER , end = "\n \n " , soft_wrap = False )
4190- for category in sorted (cmds_cats .keys (), key = self .default_sort_key ):
4191- self ._print_topics (category , cmds_cats [category ], verbose )
4192- self ._print_topics (self .default_category , cmds_doc , verbose )
4193-
4194- self .print_topics (self .misc_header , help_topics , 15 , 80 )
4195- self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
4196-
4197- def _build_command_info (self ) -> tuple [dict [str , list [str ]], list [str ], list [str ], list [str ]]:
4198- # Get a sorted list of help topics
4199- help_topics = sorted (self .get_help_topics (), key = self .default_sort_key )
4200-
4201- # Get a sorted list of visible command names
4202- visible_commands = sorted (self .get_visible_commands (), key = self .default_sort_key )
4203- cmds_doc : list [str ] = []
4204- cmds_undoc : list [str ] = []
4205- cmds_cats : dict [str , list [str ]] = {}
4206- for command in visible_commands :
4207- func = cast (CommandFunc , self .cmd_func (command ))
4208- has_help_func = False
4209- has_parser = func in self ._command_parsers
4210-
4211- if command in help_topics :
4212- # Prevent the command from showing as both a command and help topic in the output
4213- help_topics .remove (command )
4214-
4215- # Non-argparse commands can have help_functions for their documentation
4216- has_help_func = not has_parser
4217-
4218- if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
4219- category : str = getattr (func , constants .CMD_ATTR_HELP_CATEGORY )
4220- cmds_cats .setdefault (category , [])
4221- cmds_cats [category ].append (command )
4222- elif func .__doc__ or has_help_func or has_parser :
4223- cmds_doc .append (command )
4224- else :
4225- cmds_undoc .append (command )
4226- return cmds_cats , cmds_doc , cmds_undoc , help_topics
4227-
4228- def _print_topics (self , header : str , cmds : list [str ], verbose : bool ) -> None :
4229- """Print topics, switching between verbose or traditional output."""
4174+ def _print_documented_command_topics (self , header : str , cmds : list [str ], verbose : bool ) -> None :
4175+ """Print topics which are documented commands, switching between verbose or traditional output."""
42304176 import io
42314177
42324178 if cmds :
42334179 if not verbose :
42344180 self .print_topics (header , cmds , 15 , 80 )
42354181 else :
42364182 category_grid = Table .grid ()
4237- category_grid .add_row (header , style = Cmd2Style .HELP_TITLE )
4183+ category_grid .add_row (header , style = Cmd2Style .HELP_HEADER )
42384184 category_grid .add_row (Rule (characters = self .ruler ))
42394185 topics_table = Table (
42404186 Column ("Name" , no_wrap = True ),
@@ -4283,6 +4229,58 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
42834229 category_grid .add_row (topics_table )
42844230 self .poutput (category_grid , "" )
42854231
4232+ def columnize (self , str_list : list [str ] | None , display_width : int = 80 ) -> None :
4233+ """Display a list of single-line strings as a compact set of columns.
4234+
4235+ Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters.
4236+
4237+ Each column is only as wide as necessary.
4238+ Columns are separated by two spaces (one was not legible enough).
4239+ """
4240+ if not str_list :
4241+ self .poutput ("<empty>" )
4242+ return
4243+
4244+ size = len (str_list )
4245+ if size == 1 :
4246+ self .poutput (str_list [0 ])
4247+ return
4248+ # Try every row count from 1 upwards
4249+ for nrows in range (1 , len (str_list )):
4250+ ncols = (size + nrows - 1 ) // nrows
4251+ colwidths = []
4252+ totwidth = - 2
4253+ for col in range (ncols ):
4254+ colwidth = 0
4255+ for row in range (nrows ):
4256+ i = row + nrows * col
4257+ if i >= size :
4258+ break
4259+ x = str_list [i ]
4260+ colwidth = max (colwidth , su .str_width (x ))
4261+ colwidths .append (colwidth )
4262+ totwidth += colwidth + 2
4263+ if totwidth > display_width :
4264+ break
4265+ if totwidth <= display_width :
4266+ break
4267+ else :
4268+ # The output is wider than display_width. Print 1 column with each string on its own row.
4269+ nrows = len (str_list )
4270+ ncols = 1
4271+ colwidths = [1 ]
4272+ for row in range (nrows ):
4273+ texts = []
4274+ for col in range (ncols ):
4275+ i = row + nrows * col
4276+ x = "" if i >= size else str_list [i ]
4277+ texts .append (x )
4278+ while texts and not texts [- 1 ]:
4279+ del texts [- 1 ]
4280+ for col in range (len (texts )):
4281+ texts [col ] = su .align_left (texts [col ], width = colwidths [col ])
4282+ self .poutput (" " .join (texts ))
4283+
42864284 @staticmethod
42874285 def _build_shortcuts_parser () -> Cmd2ArgumentParser :
42884286 return argparse_custom .DEFAULT_ARGUMENT_PARSER (description = "List available shortcuts." )
0 commit comments