From ead41c3e3a3703746b034b02438c596166446883 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 28 Feb 2026 08:48:19 -0500 Subject: [PATCH] bottom toolbar format string for customization Like the prompt, the bottom toolbar can be customized, using the same format strings, with a special format string \B to represent the standard toolbar (in the first position only). When \B is included the user's customizations appear on the second line. When \B is not included, the user may override the toolbar completely. Transient notices will still appear to the right of the user's format. Like the prompt, both a CLI option --toolbar and a ~/.myclirc option are provided, with the CLI option taking precedence. --- changelog.md | 1 + mycli/clitoolbar.py | 39 +++++++++++++++++++++++++++------------ mycli/main.py | 20 +++++++++++++++++++- mycli/myclirc | 11 +++++++++++ test/myclirc | 11 +++++++++++ 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 6aa0157d..bb79cc6e 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features * Offer filename completions on more special commands, such as `\edit`. * Allow styling of status, timing, and warnings text. * Set up customization of prompt/continuation colors in `~/.myclirc`. +* Allow customization of the toolbar with prompt format strings. Bug Fixes diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index 0ce5c3fe..1112d30a 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -2,18 +2,20 @@ from prompt_toolkit.application import get_app from prompt_toolkit.enums import EditingMode +from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.key_binding.vi_state import InputMode from mycli.packages import special -def create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callable) -> Callable: +def create_toolbar_tokens_func(mycli, show_initial_toolbar_help: Callable, format_string: str | None) -> Callable: """Return a function that generates the toolbar tokens.""" def get_toolbar_tokens() -> list[tuple[str, str]]: divider = ('class:bottom-toolbar', ' │ ') result = [("class:bottom-toolbar", "[Tab] Complete")] + dynamic = [] result.append(divider) result.append(("class:bottom-toolbar", "[F1] Help")) @@ -42,26 +44,39 @@ def get_toolbar_tokens() -> list[tuple[str, str]]: result.append(("class:bottom-toolbar.on", _get_vi_mode())) if mycli.toolbar_error_message: - result.append(divider) - result.append(("class:bottom-toolbar.transaction.failed", mycli.toolbar_error_message)) + dynamic.append(divider) + dynamic.append(("class:bottom-toolbar.transaction.failed", mycli.toolbar_error_message)) mycli.toolbar_error_message = None if mycli.multi_line: delimiter = special.get_current_delimiter() if delimiter != ';' or show_initial_toolbar_help(): - result.append(divider) - result.append(('class:bottom-toolbar', '"')) - result.append(('class:bottom-toolbar.on', delimiter)) - result.append(('class:bottom-toolbar', '" ends a statement')) + dynamic.append(divider) + dynamic.append(('class:bottom-toolbar', '"')) + dynamic.append(('class:bottom-toolbar.on', delimiter)) + dynamic.append(('class:bottom-toolbar', '" ends a statement')) if show_initial_toolbar_help(): - result.append(divider) - result.append(("class:bottom-toolbar", "right-arrow accepts full-line suggestion")) + dynamic.append(divider) + dynamic.append(("class:bottom-toolbar", "right-arrow accepts full-line suggestion")) if mycli.completion_refresher.is_refreshing(): - result.append(divider) - result.append(("class:bottom-toolbar", "Refreshing completions…")) - + dynamic.append(divider) + dynamic.append(("class:bottom-toolbar", "Refreshing completions…")) + + if format_string and format_string != r'\B': + if format_string.startswith(r'\B'): + amended_format = format_string[2:] + result.extend(dynamic) + dynamic = [] + result.append(('class:bottom-toolbar', '\n')) + else: + amended_format = format_string + result = [] + formatted = to_formatted_text(mycli.get_custom_toolbar(amended_format), style='class:bottom-toolbar') + result.extend([*formatted]) # coerce to list for mypy + + result.extend(dynamic) return result return get_toolbar_tokens diff --git a/mycli/main.py b/mycli/main.py index 3d0a5b0f..14000fa8 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -158,6 +158,7 @@ def __init__( self, sqlexecute: SQLExecute | None = None, prompt: str | None = None, + toolbar_format: str | None = None, logfile: TextIOWrapper | Literal[False] | None = None, defaults_suffix: str | None = None, defaults_file: str | None = None, @@ -279,6 +280,7 @@ def __init__( self.min_completion_trigger = c["main"].as_int("min_completion_trigger") MIN_COMPLETION_TRIGGER = self.min_completion_trigger self.last_prompt_message = ANSI('') + self.last_custom_toolbar_message = ANSI('') # Register custom special commands. self.register_special_commands() @@ -302,6 +304,7 @@ def __init__( prompt_cnf = self.read_my_cnf(self.my_cnf, ["prompt"])["prompt"] self.prompt_format = prompt or prompt_cnf or c["main"]["prompt"] or self.default_prompt self.multiline_continuation_char = c["main"]["prompt_continuation"] + self.toolbar_format = toolbar_format or c['main']['toolbar'] self.prompt_app = None self.destructive_keywords = [ keyword for keyword in c["main"].get("destructive_keywords", "DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE").split(' ') if keyword @@ -1257,7 +1260,11 @@ def one_iteration(text: str | None = None) -> None: query = Query(text, successful, mutating) self.query_history.append(query) - get_toolbar_tokens = create_toolbar_tokens_func(self, show_initial_toolbar_help) + get_toolbar_tokens = create_toolbar_tokens_func( + self, + show_initial_toolbar_help, + self.toolbar_format, + ) if self.wider_completion_menu: complete_style = CompleteStyle.MULTI_COLUMN else: @@ -1524,6 +1531,14 @@ def get_completions(self, text: str, cursor_position: int) -> Iterable[Completio with self._completer_lock: return self.completer.get_completions(Document(text=text, cursor_position=cursor_position), None) + def get_custom_toolbar(self, toolbar_format: str) -> ANSI: + if self.prompt_app and self.prompt_app.app.current_buffer.text: + return self.last_custom_toolbar_message + toolbar = self.get_prompt(toolbar_format) + toolbar = toolbar.replace("\\x1b", "\x1b") + self.last_custom_toolbar_message = ANSI(toolbar) + return self.last_custom_toolbar_message + # todo: time/uptime update on every character typed, instead of after every return def get_prompt(self, string: str) -> str: sqlexecute = self.sqlexecute @@ -1778,6 +1793,7 @@ def get_last_query(self) -> str | None: @click.option("--list-ssh-config", "list_ssh_config", is_flag=True, help="list ssh configurations in the ssh config (requires paramiko).") @click.option("--ssh-warning-off", is_flag=True, help="Suppress the SSH deprecation notice.") @click.option("-R", "--prompt", "prompt", help=f'Prompt format (Default: "{MyCli.default_prompt}").') +@click.option('--toolbar', 'toolbar_format', help='Toolbar format.') @click.option("-l", "--logfile", type=click.File(mode="a", encoding="utf-8"), help="Log every query and its results to a file.") @click.option( "--checkpoint", type=click.File(mode="a", encoding="utf-8"), help="In batch or --execute mode, log successful queries to a file." @@ -1838,6 +1854,7 @@ def cli( dbname: str | None, verbose: bool, prompt: str | None, + toolbar_format: str | None, logfile: TextIOWrapper | None, checkpoint: TextIOWrapper | None, defaults_group_suffix: str | None, @@ -1938,6 +1955,7 @@ def get_password_from_file(password_file: str | None) -> str | None: mycli = MyCli( prompt=prompt, + toolbar_format=toolbar_format, logfile=logfile, defaults_suffix=defaults_group_suffix, defaults_file=defaults_file, diff --git a/mycli/myclirc b/mycli/myclirc index 6fc37bbe..d2f3efb9 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -125,6 +125,17 @@ wider_completion_menu = False prompt = '\t \u@\h:\d> ' prompt_continuation = '->' +# Use the same prompt format strings to construct a status line in the toolbar, +# where \B in the first position refers to the default toolbar showing keystrokes +# and state. Example: +# +# toolbar = '\B\d \D' +# +# If \B is included, the additional content will begin on the next line. More +# lines can be added with \n. If \B is not included, the customized toolbar +# can be a single line. +toolbar = '' + # Skip intro info on startup and outro info on exit less_chatty = False diff --git a/test/myclirc b/test/myclirc index 383cdcef..82f8f870 100644 --- a/test/myclirc +++ b/test/myclirc @@ -123,6 +123,17 @@ wider_completion_menu = False prompt = "\t \u@\h:\d> " prompt_continuation = -> +# Use the same prompt format strings to construct a status line in the toolbar, +# where \B in the first position refers to the default toolbar showing keystrokes +# and state. Example: +# +# toolbar = '\B\d \D' +# +# If \B is included, the additional content will begin on the next line. More +# lines can be added with \n. If \B is not included, the customized toolbar +# can be a single line. +toolbar = '' + # Skip intro info on startup and outro info on exit less_chatty = True