131131 rl_warning ,
132132 vt100_support ,
133133)
134+ from .table_creator import (
135+ Column ,
136+ SimpleTable ,
137+ )
134138from .utils import (
135139 Settable ,
136140 get_defining_class ,
@@ -2024,7 +2028,7 @@ def _perform_completion(
20242028 def complete ( # type: ignore[override]
20252029 self , text : str , state : int , custom_settings : Optional [utils .CustomCompletionSettings ] = None
20262030 ) -> Optional [str ]:
2027- """Override of cmd2 's complete method which returns the next possible completion for 'text'
2031+ """Override of cmd 's complete method which returns the next possible completion for 'text'
20282032
20292033 This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
20302034 until it returns a non-string value. It should return the next possible completion starting with text.
@@ -2159,17 +2163,44 @@ def get_visible_commands(self) -> List[str]:
21592163 if command not in self .hidden_commands and command not in self .disabled_commands
21602164 ]
21612165
2166+ # Table displayed when tab completing aliases
2167+ _alias_completion_table = SimpleTable ([Column ('Value' , width = 80 )], divider_char = None )
2168+
21622169 def _get_alias_completion_items (self ) -> List [CompletionItem ]:
2163- """Return list of current alias names and values as CompletionItems"""
2164- return [CompletionItem (cur_key , self .aliases [cur_key ]) for cur_key in self .aliases ]
2170+ """Return list of alias names and values as CompletionItems"""
2171+ results : List [CompletionItem ] = []
2172+
2173+ for cur_key in self .aliases :
2174+ row_data = [self .aliases [cur_key ]]
2175+ results .append (CompletionItem (cur_key , self ._alias_completion_table .generate_data_row (row_data )))
2176+
2177+ return results
2178+
2179+ # Table displayed when tab completing macros
2180+ _macro_completion_table = SimpleTable ([Column ('Value' , width = 80 )], divider_char = None )
21652181
21662182 def _get_macro_completion_items (self ) -> List [CompletionItem ]:
2167- """Return list of current macro names and values as CompletionItems"""
2168- return [CompletionItem (cur_key , self .macros [cur_key ].value ) for cur_key in self .macros ]
2183+ """Return list of macro names and values as CompletionItems"""
2184+ results : List [CompletionItem ] = []
2185+
2186+ for cur_key in self .macros :
2187+ row_data = [self .macros [cur_key ].value ]
2188+ results .append (CompletionItem (cur_key , self ._macro_completion_table .generate_data_row (row_data )))
2189+
2190+ return results
2191+
2192+ # Table displayed when tab completing Settables
2193+ _settable_completion_table = SimpleTable ([Column ('Value' , width = 30 ), Column ('Description' , width = 60 )], divider_char = None )
21692194
21702195 def _get_settable_completion_items (self ) -> List [CompletionItem ]:
2171- """Return list of current settable names and descriptions as CompletionItems"""
2172- return [CompletionItem (cur_key , self .settables [cur_key ].description ) for cur_key in self .settables ]
2196+ """Return list of Settable names, values, and descriptions as CompletionItems"""
2197+ results : List [CompletionItem ] = []
2198+
2199+ for cur_key in self .settables :
2200+ row_data = [self .settables [cur_key ].get_value (), self .settables [cur_key ].description ]
2201+ results .append (CompletionItem (cur_key , self ._settable_completion_table .generate_data_row (row_data )))
2202+
2203+ return results
21732204
21742205 def _get_commands_aliases_and_macros_for_completion (self ) -> List [str ]:
21752206 """Return a list of visible commands, aliases, and macros for tab completion"""
@@ -3172,7 +3203,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:
31723203 nargs = argparse .ZERO_OR_MORE ,
31733204 help = 'alias(es) to delete' ,
31743205 choices_provider = _get_alias_completion_items ,
3175- descriptive_header = 'Value' ,
3206+ descriptive_header = _alias_completion_table . generate_header () ,
31763207 )
31773208
31783209 @as_subcommand_to ('alias' , 'delete' , alias_delete_parser , help = alias_delete_help )
@@ -3206,7 +3237,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
32063237 nargs = argparse .ZERO_OR_MORE ,
32073238 help = 'alias(es) to list' ,
32083239 choices_provider = _get_alias_completion_items ,
3209- descriptive_header = 'Value' ,
3240+ descriptive_header = _alias_completion_table . generate_header () ,
32103241 )
32113242
32123243 @as_subcommand_to ('alias' , 'list' , alias_list_parser , help = alias_list_help )
@@ -3400,7 +3431,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
34003431 nargs = argparse .ZERO_OR_MORE ,
34013432 help = 'macro(s) to delete' ,
34023433 choices_provider = _get_macro_completion_items ,
3403- descriptive_header = 'Value' ,
3434+ descriptive_header = _macro_completion_table . generate_header () ,
34043435 )
34053436
34063437 @as_subcommand_to ('macro' , 'delete' , macro_delete_parser , help = macro_delete_help )
@@ -3434,7 +3465,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
34343465 nargs = argparse .ZERO_OR_MORE ,
34353466 help = 'macro(s) to list' ,
34363467 choices_provider = _get_macro_completion_items ,
3437- descriptive_header = 'Value' ,
3468+ descriptive_header = _macro_completion_table . generate_header () ,
34383469 )
34393470
34403471 @as_subcommand_to ('macro' , 'list' , macro_list_parser , help = macro_list_help )
@@ -3556,6 +3587,80 @@ def do_help(self, args: argparse.Namespace) -> None:
35563587 # Set apply_style to False so help_error's style is not overridden
35573588 self .perror (err_msg , apply_style = False )
35583589
3590+ def print_topics (self , header : str , cmds : Optional [List [str ]], cmdlen : int , maxcol : int ) -> None :
3591+ """
3592+ Print groups of commands and topics in columns and an optional header
3593+ Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters
3594+
3595+ :param header: string to print above commands being printed
3596+ :param cmds: list of topics to print
3597+ :param cmdlen: unused, even by cmd's version
3598+ :param maxcol: max number of display columns to fit into
3599+ """
3600+ if cmds :
3601+ self .poutput (header )
3602+ if self .ruler :
3603+ divider = utils .align_left ('' , fill_char = self .ruler , width = ansi .widest_line (header ))
3604+ self .poutput (divider )
3605+ self .columnize (cmds , maxcol - 1 )
3606+ self .poutput ()
3607+
3608+ def columnize (self , str_list : Optional [List [str ]], display_width : int = 80 ) -> None :
3609+ """Display a list of single-line strings as a compact set of columns.
3610+ Override of cmd's print_topics() to handle strings with ANSI style sequences and wide characters
3611+
3612+ Each column is only as wide as necessary.
3613+ Columns are separated by two spaces (one was not legible enough).
3614+ """
3615+ if not str_list :
3616+ self .poutput ("<empty>" )
3617+ return
3618+
3619+ nonstrings = [i for i in range (len (str_list )) if not isinstance (str_list [i ], str )]
3620+ if nonstrings :
3621+ raise TypeError (f"str_list[i] not a string for i in { nonstrings } " )
3622+ size = len (str_list )
3623+ if size == 1 :
3624+ self .poutput (str_list [0 ])
3625+ return
3626+ # Try every row count from 1 upwards
3627+ for nrows in range (1 , len (str_list )):
3628+ ncols = (size + nrows - 1 ) // nrows
3629+ colwidths = []
3630+ totwidth = - 2
3631+ for col in range (ncols ):
3632+ colwidth = 0
3633+ for row in range (nrows ):
3634+ i = row + nrows * col
3635+ if i >= size :
3636+ break
3637+ x = str_list [i ]
3638+ colwidth = max (colwidth , ansi .style_aware_wcswidth (x ))
3639+ colwidths .append (colwidth )
3640+ totwidth += colwidth + 2
3641+ if totwidth > display_width :
3642+ break
3643+ if totwidth <= display_width :
3644+ break
3645+ else :
3646+ nrows = len (str_list )
3647+ ncols = 1
3648+ colwidths = [0 ]
3649+ for row in range (nrows ):
3650+ texts = []
3651+ for col in range (ncols ):
3652+ i = row + nrows * col
3653+ if i >= size :
3654+ x = ""
3655+ else :
3656+ x = str_list [i ]
3657+ texts .append (x )
3658+ while texts and not texts [- 1 ]:
3659+ del texts [- 1 ]
3660+ for col in range (len (texts )):
3661+ texts [col ] = utils .align_left (texts [col ], width = colwidths [col ])
3662+ self .poutput (" " .join (texts ))
3663+
35593664 def _help_menu (self , verbose : bool = False ) -> None :
35603665 """Show a list of commands which help can be displayed for"""
35613666 cmds_cats , cmds_doc , cmds_undoc , help_topics = self ._build_command_info ()
@@ -3613,25 +3718,26 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36133718 if not verbose :
36143719 self .print_topics (header , cmds , 15 , 80 )
36153720 else :
3616- self .poutput (f'{ header } ' )
3617- widest = 0
3618- # measure the commands
3619- for command in cmds :
3620- width = ansi .style_aware_wcswidth (command )
3621- if width > widest :
3622- widest = width
3623- # add a 4-space pad
3624- widest += 4
3625- if widest < 20 :
3626- widest = 20
3627-
3628- if self .ruler :
3629- ruler_line = utils .align_left ('' , width = 80 , fill_char = self .ruler )
3630- self .poutput (f'{ ruler_line } ' )
3721+ # Find the widest command
3722+ widest = max ([ansi .style_aware_wcswidth (command ) for command in cmds ])
3723+
3724+ # Define the table structure
3725+ name_column = Column ('' , width = max (widest , 20 ))
3726+ desc_column = Column ('' , width = 80 )
3727+
3728+ topic_table = SimpleTable ([name_column , desc_column ], divider_char = self .ruler )
3729+
3730+ # Build the topic table
3731+ table_str_buf = io .StringIO ()
3732+ if header :
3733+ table_str_buf .write (header + "\n " )
3734+
3735+ divider = topic_table .generate_divider ()
3736+ if divider :
3737+ table_str_buf .write (divider + "\n " )
36313738
36323739 # Try to get the documentation string for each command
36333740 topics = self .get_help_topics ()
3634-
36353741 for command in cmds :
36363742 cmd_func = self .cmd_func (command )
36373743 doc : Optional [str ]
@@ -3658,10 +3764,8 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36583764 doc = cmd_func .__doc__
36593765
36603766 # Attempt to locate the first documentation block
3661- if not doc :
3662- doc_block = ['' ]
3663- else :
3664- doc_block = []
3767+ cmd_desc = ''
3768+ if doc :
36653769 found_first = False
36663770 for doc_line in doc .splitlines ():
36673771 stripped_line = doc_line .strip ()
@@ -3671,15 +3775,18 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36713775 if found_first :
36723776 break
36733777 elif stripped_line :
3674- doc_block .append (stripped_line )
3778+ if found_first :
3779+ cmd_desc += "\n "
3780+ cmd_desc += stripped_line
36753781 found_first = True
36763782 elif found_first :
36773783 break
36783784
3679- for doc_line in doc_block :
3680- self .poutput (f'{ command : <{widest }} { doc_line } ' )
3681- command = ''
3682- self .poutput ()
3785+ # Add this command to the table
3786+ table_row = topic_table .generate_data_row ([command , cmd_desc ])
3787+ table_str_buf .write (table_row + '\n ' )
3788+
3789+ self .poutput (table_str_buf .getvalue ())
36833790
36843791 shortcuts_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = "List available shortcuts" )
36853792
@@ -3806,15 +3913,12 @@ def complete_set_value(
38063913 "Call with just param to view that parameter's value."
38073914 )
38083915 set_parser_parent = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = set_description , add_help = False )
3809- set_parser_parent .add_argument (
3810- '-v' , '--verbose' , action = 'store_true' , help = 'include description of parameters when viewing'
3811- )
38123916 set_parser_parent .add_argument (
38133917 'param' ,
38143918 nargs = argparse .OPTIONAL ,
38153919 help = 'parameter to set or view' ,
38163920 choices_provider = _get_settable_completion_items ,
3817- descriptive_header = 'Description' ,
3921+ descriptive_header = _settable_completion_table . generate_header () ,
38183922 )
38193923
38203924 # Create the parser for the set command
@@ -3856,21 +3960,25 @@ def do_set(self, args: argparse.Namespace) -> None:
38563960 # Show all settables
38573961 to_show = list (self .settables .keys ())
38583962
3859- # Build the result strings
3860- max_len = 0
3861- results = dict ()
3862- for param in to_show :
3963+ # Define the table structure
3964+ name_label = 'Name'
3965+ max_name_width = max ([ansi .style_aware_wcswidth (param ) for param in to_show ])
3966+ max_name_width = max (max_name_width , ansi .style_aware_wcswidth (name_label ))
3967+
3968+ cols : List [Column ] = [
3969+ Column (name_label , width = max_name_width ),
3970+ Column ('Value' , width = 30 ),
3971+ Column ('Description' , width = 60 ),
3972+ ]
3973+
3974+ table = SimpleTable (cols , divider_char = self .ruler )
3975+ self .poutput (table .generate_header ())
3976+
3977+ # Build the table
3978+ for param in sorted (to_show , key = self .default_sort_key ):
38633979 settable = self .settables [param ]
3864- results [param ] = f"{ param } : { settable .get_value ()!r} "
3865- max_len = max (max_len , ansi .style_aware_wcswidth (results [param ]))
3866-
3867- # Display the results
3868- for param in sorted (results , key = self .default_sort_key ):
3869- result_str = results [param ]
3870- if args .verbose :
3871- self .poutput (f'{ utils .align_left (result_str , width = max_len )} # { self .settables [param ].description } ' )
3872- else :
3873- self .poutput (result_str )
3980+ row_data = [param , settable .get_value (), settable .description ]
3981+ self .poutput (table .generate_data_row (row_data ))
38743982
38753983 shell_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = "Execute a command as if at the OS prompt" )
38763984 shell_parser .add_argument ('command' , help = 'the command to run' , completer = shell_cmd_complete )
@@ -4070,7 +4178,6 @@ def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]:
40704178 If pyscript is None, then this function runs an interactive Python shell.
40714179 Otherwise, it runs the pyscript file.
40724180
4073- :param args: Namespace of args on the command line
40744181 :param pyscript: optional path to a pyscript file to run. This is intended only to be used by do_run_pyscript()
40754182 after it sets up sys.argv for the script. (Defaults to None)
40764183 :return: True if running of commands should stop
0 commit comments