Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
Upcoming (TBD)
==============

Features
---------
* Dynamic terminal titles based on prompt format strings.


Internal
---------
* Require a more recent version of the `wcwidth` library.



1.61.0 (2026/03/07)
==============

Expand Down
70 changes: 70 additions & 0 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import random
import re
import shutil
import subprocess
import sys
import threading
import traceback
Expand Down Expand Up @@ -79,6 +80,7 @@
from mycli.packages.special.main import ArgType
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
from mycli.packages.sqlresult import SQLResult
from mycli.packages.string_utils import sanitize_terminal_title
from mycli.packages.tabular_output import sql_format
from mycli.packages.toolkit.history import FileHistoryWithTimestamp
from mycli.sqlcompleter import SQLCompleter
Expand Down Expand Up @@ -308,6 +310,10 @@ def __init__(
self.prompt_lines = 0
self.multiline_continuation_char = c["main"]["prompt_continuation"]
self.toolbar_format = toolbar_format or c['main']['toolbar']
self.terminal_tab_title_format = c['main']['terminal_tab_title']
self.terminal_window_title_format = c['main']['terminal_window_title']
self.multiplex_window_title_format = c['main']['multiplex_window_title']
self.multiplex_pane_title_format = c['main']['multiplex_pane_title']
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
Expand Down Expand Up @@ -429,6 +435,8 @@ def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:
self.sqlexecute.change_db(arg)
msg = f'You are now connected to database "{self.sqlexecute.dbname}" as user "{self.sqlexecute.user}"'

self.set_all_external_titles()

yield SQLResult(status=msg)

def execute_from_file(self, arg: str, **_) -> Iterable[SQLResult]:
Expand Down Expand Up @@ -1318,6 +1326,8 @@ def one_iteration(text: str | None = None) -> None:
else:
self.prompt_app.app.ttimeoutlen = self.emacs_ttimeoutlen

self.set_all_external_titles()

try:
while True:
one_iteration()
Expand Down Expand Up @@ -1549,6 +1559,66 @@ 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 set_all_external_titles(self) -> None:
self.set_external_terminal_tab_title()
self.set_external_terminal_window_title()
self.set_external_multiplex_window_title()
self.set_external_multiplex_pane_title()

def set_external_terminal_tab_title(self) -> None:
if not self.terminal_tab_title_format:
return
if not self.prompt_app:
return
if not sys.stderr.isatty():
return
title = sanitize_terminal_title(self.get_prompt(self.terminal_tab_title_format, self.prompt_app.app.render_counter))
print(f'\x1b]1;{title}\a', file=sys.stderr, end='')
sys.stderr.flush()

def set_external_terminal_window_title(self) -> None:
if not self.terminal_window_title_format:
return
if not self.prompt_app:
return
if not sys.stderr.isatty():
return
title = sanitize_terminal_title(self.get_prompt(self.terminal_window_title_format, self.prompt_app.app.render_counter))
print(f'\x1b]2;{title}\a', file=sys.stderr, end='')
sys.stderr.flush()

def set_external_multiplex_window_title(self) -> None:
if not self.multiplex_window_title_format:
return
if not os.getenv('TMUX'):
return
if not self.prompt_app:
return
title = sanitize_terminal_title(self.get_prompt(self.multiplex_window_title_format, self.prompt_app.app.render_counter))
try:
subprocess.run(
['tmux', 'rename-window', title],
check=False,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError:
pass

def set_external_multiplex_pane_title(self) -> None:
if not self.multiplex_pane_title_format:
return
if not os.getenv('TMUX'):
return
if not self.prompt_app:
return
if not sys.stderr.isatty():
return
title = sanitize_terminal_title(self.get_prompt(self.multiplex_pane_title_format, self.prompt_app.app.render_counter))
print(f'\x1b]2;{title}\x1b\\', file=sys.stderr, end='')
sys.stderr.flush()

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
Expand Down
22 changes: 22 additions & 0 deletions mycli/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ prompt_continuation = '->'
# can be a single line.
toolbar = ''

# Use the same prompt format strings to construct a terminal tab title.
# The original XTerm docs call this title the "window title", but it now
# probably refers to a terminal tab. This title is only updated as frequently
# as the database is changed.
terminal_tab_title = ''

# Use the same prompt format strings to construct a terminal window title.
# The original XTerm docs call this title the "icon title", but it now
# probably refers to a terminal window which contains tabs. This title is
# only updated as frequently as the database is changed.
terminal_window_title = ''

# Use the same prompt format strings to construct a window title in a terminal
# multiplexer. Currently only tmux is supported. This title is only updated
# as frequently as the database is changed.
multiplex_window_title = ''

# Use the same prompt format strings to construct a pane title in a terminal
# multiplexer. Currently only tmux is supported. This title is only updated
# as frequently as the database is changed.
multiplex_pane_title = ''

# Skip intro info on startup and outro info on exit
less_chatty = False

Expand Down
10 changes: 10 additions & 0 deletions mycli/packages/string_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import re

from cli_helpers.utils import strip_ansi


def sanitize_terminal_title(title: str) -> str:
sanitized = strip_ansi(title)
sanitized = sanitized.replace('\n', ' ')
sanitized = re.sub('[\x00-\x1f\x7f]', '', sanitized)
return sanitized
22 changes: 22 additions & 0 deletions test/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,28 @@ prompt_continuation = ->
# can be a single line.
toolbar = ''

# Use the same prompt format strings to construct a terminal tab title.
# The original XTerm docs call this title the "window title", but it now
# probably refers to a terminal tab. This title is only updated as frequently
# as the database is changed.
terminal_tab_title = ''

# Use the same prompt format strings to construct a terminal window title.
# The original XTerm docs call this title the "icon title", but it now
# probably refers to a terminal window which contains tabs. This title is
# only updated as frequently as the database is changed.
terminal_window_title = ''

# Use the same prompt format strings to construct a window title in a terminal
# multiplexer. Currently only tmux is supported. This title is only updated
# as frequently as the database is changed.
multiplex_window_title = ''

# Use the same prompt format strings to construct a pane title in a terminal
# multiplexer. Currently only tmux is supported. This title is only updated
# as frequently as the database is changed.
multiplex_pane_title = ''

# Skip intro info on startup and outro info on exit
less_chatty = True

Expand Down