From 154483030712f6f9cb49b2771aa2f8c60aeb4659 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 30 Aug 2025 08:48:47 +0530 Subject: [PATCH] Allow using a custom python function in tab_title_template Makes it easier to do complex processing --- docs/changelog.rst | 3 +++ kitty/options/definition.py | 8 ++++++++ kitty/tab_bar.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 46494e529..9be526fe0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -140,6 +140,9 @@ Detailed list of changes controlled by :opt:`cursor_blink_interval` and :opt:`cursor_stop_blinking_after`. (:pull:`8551`) +- Allow using a custom python function to draw tab titles in the tab bar, see + :opt:`tab_title_template` + - Wayland: Fix incorrect window size calculation when transitioning from full screen to non-full screen with client side decorations (:iss:`8826`) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index fe32de614..23c388ab2 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1511,6 +1511,14 @@ use :code:`{sup.index}`. All data available is: :code:`tab.progress_percent` If a command running in a window reports the progress for a task, show this progress as a percentage from all windows in the tab, averaged. Empty string is no progress is reported. +:code:`custom` + This will call a function named :code:`draw_title(data)` from the file :file:`tab_bar.py` placed in + the kitty config directory. The function will be passed a dictionary of data, the same data that + can be used in this template. It can then perform arbitrarily complex processing and return a string. + For example: :code:`tab_title_template "{custom}"` will use the output of the function as the tab title. + Any print statements in the :code:`draw_title()` will print to the STDOUT of the kitty process, useful + for debugging. + Note that formatting is done by Python's string formatting machinery, so you can use, for instance, :code:`{layout_name[:2].upper()}` to show only the first two diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index d65528e73..097e9dfb3 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -303,6 +303,7 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int, prefix += '{activity_symbol}' if prefix: template = '{fmt.fg.red}' + prefix + '{fmt.fg.tab}' + template + eval_locals['custom'] = load_custom_draw_title(eval_locals) try: title = eval(compile_template(template), {'__builtins__': safe_builtins}, eval_locals) except Exception as e: @@ -486,15 +487,24 @@ def draw_tab_with_powerline( @run_once -def load_custom_draw_tab() -> DrawTabFunc: +def load_custom_draw_tab_module() -> dict[str, Any]: import runpy import traceback try: - m = runpy.run_path(os.path.join(config_dir, 'tab_bar.py')) - func: DrawTabFunc = m['draw_tab'] + return runpy.run_path(os.path.join(config_dir, 'tab_bar.py')) + except FileNotFoundError: + return {} except Exception as e: traceback.print_exc() - log_error(f'Failed to load custom draw_tab function with error: {e}') + log_error(f'Failed to load custom tab_bar.py module with error: {e}') + return {} + + +@run_once +def load_custom_draw_tab() -> DrawTabFunc: + m = load_custom_draw_tab_module() + func: DrawTabFunc | None = m.get('draw_tab') + if func is None: return draw_tab_with_fade @wraps(func) @@ -512,6 +522,24 @@ def load_custom_draw_tab() -> DrawTabFunc: return draw_tab +class CustomDrawTitleFunc: + + def __init__(self, data: dict[str, Any], implementation: Callable[[dict[str, Any]], str] | None = None): + self._implementation = implementation + self._data = data.copy() + + def __str__(self) -> str: + if self._implementation is None: + return '' + return str(self._implementation(self._data)) + __repr__ = __str__ + + +def load_custom_draw_title(data: dict[str, Any]) -> CustomDrawTitleFunc: + m = load_custom_draw_tab_module() + return CustomDrawTitleFunc(data, m.get('draw_title')) + + class CellRange(NamedTuple): start: int end: int