Skip to content

# cmd2.pt_utils

## `` cmd2.pt_utils

Utilities for integrating prompt_toolkit with cmd2.

### `` BASE_DELIMITERS `module-attribute`

    BASE_DELIMITERS = ' \t\n' + join(QUOTES) + join(REDIRECTION_CHARS)

### `` ANSI_NAMES `module-attribute`

    ANSI_NAMES = ('ansiblack', 'ansired', 'ansigreen', 'ansiyellow', 'ansiblue', 'ansimagenta', 'ansicyan', 'ansiwhite', 'ansibrightblack', 'ansibrightred', 'ansibrightgreen', 'ansibrightyellow', 'ansibrightblue', 'ansibrightmagenta', 'ansibrightcyan', 'ansibrightwhite')

### `` Cmd2Completer

    Cmd2Completer(cmd_app, custom_settings=None)

Bases: `Completer`

Completer that delegates to cmd2's completion logic.

Initialize prompt_toolkit based completer class.

Source code in `cmd2/pt_utils.py`

    133
    134
    135
    136
    137
    138
    139
    140

    def __init__(
        self,
        cmd_app: "Cmd",
        custom_settings: utils.CustomCompletionSettings | None = None,
    ) -> None:
        """Initialize prompt_toolkit based completer class."""
        self._cmd_app = cmd_app
        self.custom_settings = custom_settings

#### `` custom_settings `instance-attribute`

    custom_settings = custom_settings

#### `` get_completions

    get_completions(document, _complete_event)

Get completions for the current input.

Source code in `cmd2/pt_utils.py`

    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230

    def get_completions(self, document: Document, _complete_event: object) -> Iterable[Completion]:
        """Get completions for the current input."""
        # Find the beginning of the current word based on delimiters
        line = document.text
        cursor_pos = document.cursor_position

        # Define delimiters for completion to match cmd2/readline behavior
        delimiters = BASE_DELIMITERS
        delimiters += "".join(self._cmd_app.statement_parser.terminators)

        # Find last delimiter before cursor to determine the word being completed
        begidx = 0
        for i in range(cursor_pos - 1, -1, -1):
            if line[i] in delimiters:
                begidx = i + 1
                break

        endidx = cursor_pos
        text = line[begidx:endidx]

        completions = self._cmd_app.complete(
            text, line=line, begidx=begidx, endidx=endidx, custom_settings=self.custom_settings
        )

        if completions.error:
            print_formatted_text(pt_filter_style(completions.error))
            return

        # Print completion table if present
        if completions.table is not None:
            console = ru.Cmd2GeneralConsole(file=self._cmd_app.stdout)
            with console.capture() as capture:
                console.print(completions.table, end="", soft_wrap=False)
            print_formatted_text(pt_filter_style("\n" + capture.get()))

        if not completions:
            # Print hint if present
            if completions.hint:
                print_formatted_text(pt_filter_style(completions.hint))
            return

        # The length of the user's input minus any shortcut.
        search_text_length = len(text) - completions._search_text_offset

        # If matches require quoting but the word isn't quoted yet, we insert the
        # opening quote directly into the buffer. We do this because if any completions
        # change text before the cursor (like prepending a quote), prompt-toolkit will
        # not return a common prefix to the command line. By modifying the buffer
        # and returning early, we trigger a new completion cycle where the quote
        # is already present, allowing for proper common prefix calculation.
        if completions._add_opening_quote and search_text_length > 0:
            buffer = get_app().current_buffer

            buffer.cursor_left(search_text_length)
            buffer.insert_text(completions._quote_char)
            buffer.cursor_right(search_text_length)
            return

        # Return the completions
        for item in completions:
            # Set offset to the start of the current word to overwrite it with the completion
            start_position = -len(text)
            match_text = item.text

            # If we need a quote but didn't interrupt (because text was empty),
            # prepend the quote here so it's included in the insertion.
            if completions._add_opening_quote:
                match_text = (
                    match_text[: completions._search_text_offset]
                    + completions._quote_char
                    + match_text[completions._search_text_offset :]
                )

            # Finalize if there's only one match
            if len(completions) == 1 and completions.allow_finalization:
                # Close any open quote
                if completions._quote_char:
                    match_text += completions._quote_char

                # Add trailing space if the cursor is at the end of the line
                if endidx == len(line):
                    match_text += " "

            yield Completion(
                match_text,
                start_position=start_position,
                display=pt_filter_style(item.display),
                display_meta=pt_filter_style(item.display_meta),
            )

### `` Cmd2History

    Cmd2History(history_strings=None)

Bases: `History`

A non-persistent, in-memory history buffer for prompt-toolkit.

This class serves as the backing store for UI history navigation (e.g., arrowing through previous commands). It explicitly avoids handling persistence, deferring all permanent storage logic to the cmd2 application.

Initialize the instance.

Source code in `cmd2/pt_utils.py`

    241
    242
    243
    244
    245
    246
    247
    248
    249
    250

    def __init__(self, history_strings: Iterable[str] | None = None) -> None:
        """Initialize the instance."""
        super().__init__()

        if history_strings:
            for string in history_strings:
                self.append_string(string)

        # Mark that self._loaded_strings is populated.
        self._loaded = True

#### `` append_string

    append_string(string)

Override to filter our consecutive duplicates.

Source code in `cmd2/pt_utils.py`

    252
    253
    254
    255
    256

    def append_string(self, string: str) -> None:
        """Override to filter our consecutive duplicates."""
        # History is sorted newest to oldest, so we compare to the first element.
        if string and (not self._loaded_strings or self._loaded_strings[0] != string):
            super().append_string(string)

#### `` store_string

    store_string(string)

No-op: Persistent history data is stored in cmd2.Cmd.history.

Source code in `cmd2/pt_utils.py`

    258
    259

    def store_string(self, string: str) -> None:
        """No-op: Persistent history data is stored in cmd2.Cmd.history."""

#### `` load_history_strings

    load_history_strings()

Yield strings from newest to oldest.

Source code in `cmd2/pt_utils.py`

    261
    262
    263

    def load_history_strings(self) -> Iterable[str]:
        """Yield strings from newest to oldest."""
        yield from self._loaded_strings

#### `` clear

    clear()

Clear the UI history navigation data.

Source code in `cmd2/pt_utils.py`

    265
    266
    267

    def clear(self) -> None:
        """Clear the UI history navigation data."""
        self._loaded_strings.clear()

### `` Cmd2Lexer

    Cmd2Lexer(cmd_app)

Bases: `Lexer`

Lexer that highlights cmd2 command names, aliases, and macros.

Initialize the Lexer.

PARAMETER | DESCRIPTION
--- | ---
`cmd_app` | cmd2.Cmd instance TYPE: `Cmd`

Source code in `cmd2/pt_utils.py`

    280
    281
    282
    283
    284
    285
    286
    287
    288
    289

    def __init__(
        self,
        cmd_app: "Cmd",
    ) -> None:
        """Initialize the Lexer.

        :param cmd_app: cmd2.Cmd instance
        """
        super().__init__()
        self._cmd_app = cmd_app

#### `` COMMAND_STYLE `class-attribute` `instance-attribute`

    COMMAND_STYLE = f'class:{LEXER_COMMAND}'

#### `` ALIAS_STYLE `class-attribute` `instance-attribute`

    ALIAS_STYLE = f'class:{LEXER_ALIAS}'

#### `` MACRO_STYLE `class-attribute` `instance-attribute`

    MACRO_STYLE = f'class:{LEXER_MACRO}'

#### `` FLAG_STYLE `class-attribute` `instance-attribute`

    FLAG_STYLE = f'class:{LEXER_FLAG}'

#### `` ARGUMENT_STYLE `class-attribute` `instance-attribute`

    ARGUMENT_STYLE = f'class:{LEXER_ARGUMENT}'

#### `` lex_document

    lex_document(document)

Lex the document.

Source code in `cmd2/pt_utils.py`

    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376

    def lex_document(self, document: Document) -> Callable[[int], Any]:
        """Lex the document."""
        # Get redirection tokens and terminators to avoid highlighting them as values
        exclude_tokens = set(constants.REDIRECTION_TOKENS)
        exclude_tokens.update(self._cmd_app.statement_parser.terminators)
        arg_pattern = re.compile(r'(\s+)|(--?[^\s\'"]+)|("[^"]*"?|\'[^\']*\'?)|([^\s\'"]+)')

        def highlight_args(text: str, tokens: list[tuple[str, str]]) -> None:
            """Highlight arguments in a string."""
            for m in arg_pattern.finditer(text):
                space, flag, quoted, word = m.groups()
                match_text = m.group(0)

                if space:
                    tokens.append(("", match_text))
                elif flag:
                    tokens.append((self.FLAG_STYLE, match_text))
                elif (quoted or word) and match_text not in exclude_tokens:
                    tokens.append((self.ARGUMENT_STYLE, match_text))
                else:
                    tokens.append(("", match_text))

        def get_line(lineno: int) -> list[tuple[str, str]]:
            """Return the tokens for the given line number."""
            line = document.lines[lineno]
            tokens: list[tuple[str, str]] = []

            # No syntax highlighting if styles are disallowed
            if ru.ALLOW_STYLE == ru.AllowStyle.NEVER:
                tokens.append(("", line))
                return tokens

            # Only attempt to match a command on the first line
            if lineno == 0:
                # Use cmd2's command pattern to find the first word (the command)
                match = self._cmd_app.statement_parser._command_pattern.search(line)
                if match:
                    # Group 1 is the command, Group 2 is the character(s) that terminated the command match
                    command = match.group(1)
                    cmd_start = match.start(1)
                    cmd_end = match.end(1)

                    # Add any leading whitespace
                    if cmd_start > 0:
                        tokens.append(("", line[:cmd_start]))

                    if command:
                        # Determine the style for the command
                        shortcut_found = False
                        for shortcut, _ in self._cmd_app.statement_parser.shortcuts:
                            if command.startswith(shortcut):
                                # Add the shortcut with the command style
                                tokens.append((self.COMMAND_STYLE, shortcut))

                                # If there's more in the command word, it's an argument
                                if len(command) > len(shortcut):
                                    tokens.append((self.ARGUMENT_STYLE, command[len(shortcut) :]))

                                shortcut_found = True
                                break

                        if not shortcut_found:
                            style = ""
                            if command in self._cmd_app.get_all_commands():
                                style = self.COMMAND_STYLE
                            elif command in self._cmd_app.aliases:
                                style = self.ALIAS_STYLE
                            elif command in self._cmd_app.macros:
                                style = self.MACRO_STYLE

                            # Add the command with the determined style
                            tokens.append((style, command))

                    # Add the rest of the line as arguments
                    if cmd_end < len(line):
                        highlight_args(line[cmd_end:], tokens)
                else:
                    # No command match found on the first line
                    tokens.append(("", line))
            else:
                # All other lines are treated as arguments
                highlight_args(line, tokens)

            return tokens

        return get_line

### `` pt_filter_style

    pt_filter_style(text)

Strip styles if disallowed by ru.ALLOW_STYLE. Otherwise return an ANSI object.

This function is intended specifically for text rendered by prompt-toolkit.

Source code in `cmd2/pt_utils.py`

    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75

    def pt_filter_style(text: str | ANSI) -> str | ANSI:
        """Strip styles if disallowed by ru.ALLOW_STYLE. Otherwise return an ANSI object.

        This function is intended specifically for text rendered by prompt-toolkit.
        """
        # We only use prompt-toolkit to write to a terminal. Therefore
        # we only have to check if ALLOW_STYLE is Never.
        if ru.ALLOW_STYLE == ru.AllowStyle.NEVER:
            raw_text = text.value if isinstance(text, ANSI) else text
            return su.strip_style(raw_text)

        # String must be an ANSI object for prompt-toolkit to render ANSI style sequences.
        return text if isinstance(text, ANSI) else ANSI(text)

### `` rich_to_pt_color `cached`

    rich_to_pt_color(color)

Convert a rich Color object to a prompt_toolkit color string.

Source code in `cmd2/pt_utils.py`

    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91

    @lru_cache(maxsize=256)
    def rich_to_pt_color(color: "Color | None") -> str:
        """Convert a rich Color object to a prompt_toolkit color string."""
        if not color or color.is_default:
            return "default"

        # Use prompt_toolkit's 16 standard ansi color names if applicable.
        # This prevents overriding terminal themes with absolute RGB values.
        if color.number is not None and 0 <= color.number <= 15:
            return ANSI_NAMES[color.number]

        # For 8-bit and truecolor, we fallback to hex RGB strings which prompt-toolkit supports natively
        c = color.get_truecolor()
        return f"#{c.red:02x}{c.green:02x}{c.blue:02x}"

### `` rich_to_pt_style `cached`

    rich_to_pt_style(rich_style)

Convert a rich Style object to a prompt_toolkit style string.

Source code in `cmd2/pt_utils.py`

     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127

    @lru_cache(maxsize=1024)
    def rich_to_pt_style(rich_style: StyleType) -> str:
        """Convert a rich Style object to a prompt_toolkit style string."""
        if not rich_style:
            return ""

        if isinstance(rich_style, str):
            rich_style = Style.parse(rich_style)

        parts = []

        fg_color = rich_to_pt_color(rich_style.color)
        parts.append(f"fg:{fg_color}")

        bg_color = rich_to_pt_color(rich_style.bgcolor)
        parts.append(f"bg:{bg_color}")

        if rich_style.bold is not None:
            parts.append("bold" if rich_style.bold else "nobold")
        if rich_style.underline is not None:
            parts.append("underline" if rich_style.underline else "nounderline")
        if rich_style.strike is not None:
            parts.append("strike" if rich_style.strike else "nostrike")
        if rich_style.italic is not None:
            parts.append("italic" if rich_style.italic else "noitalic")
        if rich_style.blink is not None:
            parts.append("blink" if rich_style.blink else "noblink")
        if rich_style.reverse is not None:
            parts.append("reverse" if rich_style.reverse else "noreverse")
        if rich_style.conceal is not None:
            parts.append("hidden" if rich_style.conceal else "nohidden")
        if rich_style.dim is not None:
            parts.append("dim" if rich_style.dim else "nodim")
        return " ".join(parts)