From ab3a8ca56ad33a2f34306648dd57e1bbaa13d0d9 Mon Sep 17 00:00:00 2001 From: mcrmck Date: Sun, 1 Feb 2026 00:37:40 -0500 Subject: [PATCH 1/5] Add pane title bar feature for window splits Add an optional title bar that displays above or below each window pane when multiple windows are visible in a tab. This is similar to tmux's pane-border-format or Terminator's pane title bars. New configuration options: - pane_title_bar: none/top/bottom (default: none) - pane_title_template: f-string template (same syntax as tab_title_template) - active_pane_title_template: override for active pane - pane_title_bar_active_fg/bg: colors for active pane title - pane_title_bar_inactive_fg/bg: colors for inactive pane titles - pane_title_bar_align: left/center/right text alignment The title bars are rendered using virtual Screen objects registered with the GPU, following the same model as the tab bar. Title bars are automatically hidden when only a single window is visible. Ref: https://github.com/kovidgoyal/kitty/discussions/9448 Co-Authored-By: Claude Opus 4.5 --- kitty/child-monitor.c | 15 + kitty/fast_data_types.pyi | 7 + kitty/layout/base.py | 20 ++ kitty/options/definition.py | 53 ++++ kitty/options/parse.py | 36 ++- kitty/options/types.py | 20 +- kitty/pane_title_bar.py | 273 +++++++++++++++++ kitty/state.c | 16 + kitty/state.h | 1 + kitty/tabs.py | 4 + tools/themes/collection.go | 572 ++++++++++++++++++------------------ 11 files changed, 731 insertions(+), 286 deletions(-) create mode 100644 kitty/pane_title_bar.py diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 504a7bd7f..6d52d970f 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -807,6 +807,12 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * if (send_cell_data_to_gpu(WD.vao_idx, WD.screen, os_window)) needs_render = true; if (WD.screen->start_visual_bell_at != 0) needs_render = true; } + // Prepare pane title bar screen data for GPU + if (w->visible && w->pane_title_render_data.screen) { + CursorRenderInfo *cri = &w->pane_title_render_data.screen->cursor_render_info; + zero_at_ptr(cri); + if (send_cell_data_to_gpu(w->pane_title_render_data.vao_idx, w->pane_title_render_data.screen, os_window)) needs_render = true; + } } return needs_render || was_previously_rendered_with_layers != os_window->needs_layers; } @@ -868,6 +874,15 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co if (WD.screen->start_visual_bell_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); } } + // Draw pane title bars + for (unsigned int i = 0; i < tab->num_windows; i++) { + Window *w = tab->windows + i; + if (w->visible && w->pane_title_render_data.screen && + w->pane_title_render_data.geometry.right > w->pane_title_render_data.geometry.left && + w->pane_title_render_data.geometry.bottom > w->pane_title_render_data.geometry.top) { + draw_cells(&w->pane_title_render_data, os_window, i == tab->active_window, true, false, NULL); + } + } setup_os_window_for_rendering(os_window, tab, active_window, false); if (global_state.thumbnail_callback.os_window == os_window->id) { thumbnail_callback(os_window); diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index d826597e7..c3a0eec68 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1404,6 +1404,13 @@ def set_tab_bar_render_data( pass +def set_pane_title_bar_render_data( + os_window_id: int, tab_id: int, window_id: int, screen: Screen, + left: int, top: int, right: int, bottom: int +) -> None: + pass + + def set_window_render_data( os_window_id: int, tab_id: int, window_id: int, screen: Screen, left: int, top: int, right: int, bottom: int, diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 62d0098dd..667fdfdf9 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -368,6 +368,26 @@ class Layout: self.update_visibility(all_windows) self.blank_rects = [] self.do_layout(all_windows) + self._apply_pane_title_bars(all_windows) + + def _apply_pane_title_bars(self, all_windows: WindowList) -> None: + opts = get_options() + position = opts.pane_title_bar + if position == 'none': + return + visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) + if len(visible_groups) < 2: + return + ch = lgd.cell_height + for wg in visible_groups: + geom = wg.geometry + if geom is None: + continue + if position == 'top': + new_geom = geom._replace(top=geom.top + ch, ynum=max(1, geom.ynum - 1)) + else: + new_geom = geom._replace(bottom=geom.bottom - ch, ynum=max(1, geom.ynum - 1)) + wg.set_geometry(new_geom) def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None: bw = 1 if self.must_draw_borders else 0 diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 0c24ca30a..409e619de 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1466,6 +1466,59 @@ dragging of borders. Note that because kitty uses layouts, dragging borders does actually resize the window itself, but instead, the layout row/column/slot, which can result in multiple windows getting resized. ''') + +opt('pane_title_bar', 'none', + choices=('none', 'top', 'bottom'), + long_text=''' +Show a title bar for each window/pane when there are multiple windows in a tab. +The title bar displays the window title and is hidden when only a single window +is visible. The value controls the position of the title bar relative to the +window content. Set to :code:`none` to disable. +''' + ) + +opt('pane_title_template', '"{title}"', + option_type='tab_title_template', + long_text=''' +A template to render the pane title bar text. Uses the same template syntax as +:opt:`tab_title_template`. Available variables include: :code:`{title}`, +:code:`{index}`, :code:`{layout_name}`, :code:`{num_windows}`, +:code:`{num_window_groups}`, :code:`{tab.active_wd}`, etc. +''' + ) + +opt('active_pane_title_template', 'none', + option_type='tab_title_template', + long_text=''' +Template to use for the active pane title bar. If not set (the value +:code:`none`), the :opt:`pane_title_template` is used. +''' + ) + +opt('pane_title_bar_active_fg', '#000000', + option_type='to_color', + long_text='Foreground color for the active pane title bar.' + ) + +opt('pane_title_bar_active_bg', '#00ff00', + option_type='to_color', + long_text='Background color for the active pane title bar.' + ) + +opt('pane_title_bar_inactive_fg', '#cccccc', + option_type='to_color', + long_text='Foreground color for inactive pane title bars.' + ) + +opt('pane_title_bar_inactive_bg', '#333333', + option_type='to_color', + long_text='Background color for inactive pane title bars.' + ) + +opt('pane_title_bar_align', 'center', + choices=('left', 'center', 'right'), + long_text='Horizontal alignment of the text in pane title bars.' + ) egr() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index ff7e3f0aa..8f8249ba3 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -36,6 +36,9 @@ class Parser: def active_border_color(self, val: str, ans: dict[str, typing.Any]) -> None: ans['active_border_color'] = to_color_or_none(val) + def active_pane_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['active_pane_title_template'] = tab_title_template(val) + def active_tab_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['active_tab_background'] = to_color(val) @@ -1165,6 +1168,37 @@ class Parser: def open_url_with(self, val: str, ans: dict[str, typing.Any]) -> None: ans['open_url_with'] = to_cmdline(val) + def pane_title_bar(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_pane_title_bar: + raise ValueError(f"The value {val} is not a valid choice for pane_title_bar") + ans["pane_title_bar"] = val + + choices_for_pane_title_bar = frozenset(('none', 'top', 'bottom')) + + def pane_title_bar_active_bg(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['pane_title_bar_active_bg'] = to_color(val) + + def pane_title_bar_active_fg(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['pane_title_bar_active_fg'] = to_color(val) + + def pane_title_bar_align(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_pane_title_bar_align: + raise ValueError(f"The value {val} is not a valid choice for pane_title_bar_align") + ans["pane_title_bar_align"] = val + + choices_for_pane_title_bar_align = frozenset(('left', 'center', 'right')) + + def pane_title_bar_inactive_bg(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['pane_title_bar_inactive_bg'] = to_color(val) + + def pane_title_bar_inactive_fg(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['pane_title_bar_inactive_fg'] = to_color(val) + + def pane_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['pane_title_template'] = tab_title_template(val) + def paste_actions(self, val: str, ans: dict[str, typing.Any]) -> None: ans['paste_actions'] = paste_actions(val) @@ -1322,7 +1356,7 @@ class Parser: raise ValueError(f"The value {val} is not a valid choice for tab_bar_align") ans["tab_bar_align"] = val - choices_for_tab_bar_align = frozenset(('left', 'center', 'right')) + choices_for_tab_bar_align = choices_for_pane_title_bar_align def tab_bar_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['tab_bar_background'] = to_color_or_none(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 8272f0c70..a6711a79a 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -25,11 +25,13 @@ choices_for_default_pointer_shape = typing.Literal['arrow', 'beam', 'text', 'poi choices_for_linux_display_server = typing.Literal['auto', 'wayland', 'x11'] choices_for_macos_colorspace = typing.Literal['srgb', 'default', 'displayp3'] choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none', 'window'] +choices_for_pane_title_bar = typing.Literal['none', 'top', 'bottom'] +choices_for_pane_title_bar_align = typing.Literal['left', 'center', 'right'] choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] -choices_for_tab_bar_align = typing.Literal['left', 'center', 'right'] +choices_for_tab_bar_align = choices_for_pane_title_bar_align choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separator', 'slant', 'custom'] choices_for_tab_powerline_style = typing.Literal['angled', 'round', 'slanted'] choices_for_tab_switch_strategy = typing.Literal['last', 'left', 'previous', 'right'] @@ -41,6 +43,7 @@ choices_for_window_logo_position = choices_for_placement_strategy option_names = ( 'action_alias', 'active_border_color', + 'active_pane_title_template', 'active_tab_background', 'active_tab_font_style', 'active_tab_foreground', @@ -405,6 +408,13 @@ option_names = ( 'narrow_symbols', 'notify_on_cmd_finish', 'open_url_with', + 'pane_title_bar', + 'pane_title_bar_active_bg', + 'pane_title_bar_active_fg', + 'pane_title_bar_align', + 'pane_title_bar_inactive_bg', + 'pane_title_bar_inactive_fg', + 'pane_title_template', 'paste_actions', 'pixel_scroll', 'placement_strategy', @@ -502,6 +512,7 @@ option_names = ( class Options: active_border_color: kitty.fast_data_types.Color | None = Color(0, 255, 0) + active_pane_title_template: str = 'none' active_tab_background: Color = Color(238, 238, 238) active_tab_font_style: tuple[bool, bool] = (True, True) active_tab_foreground: Color = Color(0, 0, 0) @@ -600,6 +611,13 @@ class Options: mouse_hide_wait: MouseHideWait = MouseHideWait(hide_wait=0.0, show_wait=0.0, show_threshold=40, scroll_show=True) if is_macos else MouseHideWait(hide_wait=3.0, show_wait=0.0, show_threshold=40, scroll_show=True) notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=(), clear_on=('focus', 'next')) open_url_with: list[str] = ['default'] + pane_title_bar: choices_for_pane_title_bar = 'none' + pane_title_bar_active_bg: Color = Color(0, 255, 0) + pane_title_bar_active_fg: Color = Color(0, 0, 0) + pane_title_bar_align: choices_for_pane_title_bar_align = 'center' + pane_title_bar_inactive_bg: Color = Color(51, 51, 51) + pane_title_bar_inactive_fg: Color = Color(204, 204, 204) + pane_title_template: str = '{title}' paste_actions: frozenset[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) pixel_scroll: bool = True placement_strategy: choices_for_placement_strategy = 'center' diff --git a/kitty/pane_title_bar.py b/kitty/pane_title_bar.py new file mode 100644 index 000000000..c3953af62 --- /dev/null +++ b/kitty/pane_title_bar.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# License: GPL v3 Copyright: 2024, kitty contributors + +from functools import lru_cache +from typing import Any, NamedTuple + +from .fast_data_types import ( + DECAWM, + Screen, + cell_size_for_window, + get_options, + set_pane_title_bar_render_data, +) +from .rgb import color_as_sgr, color_from_int, to_color +from .types import WindowGeometry +from .utils import color_as_int, log_error, sgr_sanitizer_pat +from .window_list import WindowList + + +@lru_cache +def _report_template_failure(template: str, e: str) -> None: + log_error(f'Invalid pane title template: "{template}" with error: {e}') + + +@lru_cache +def _compile_template(template: str) -> Any: + try: + return compile('f"""' + template + '"""', '', 'eval') + except Exception as e: + _report_template_failure(template, str(e)) + + +safe_builtins = { + 'max': max, 'min': min, 'str': str, 'repr': repr, 'abs': abs, + 'len': len, 'chr': chr, 'ord': ord, +} + + +class PaneTitleColorFormatter: + is_active: bool = False + + def __init__(self, which: str): + self.which = which + + def __getattr__(self, name: str) -> str: + q = name + if q == 'default': + ans = '9' + elif q == 'pane': + opts = get_options() + if self.is_active: + col = color_from_int(color_as_int(opts.pane_title_bar_active_fg if self.which == '3' else opts.pane_title_bar_active_bg)) + else: + col = color_from_int(color_as_int(opts.pane_title_bar_inactive_fg if self.which == '3' else opts.pane_title_bar_inactive_bg)) + ans = f'8{color_as_sgr(col)}' + elif q.startswith('color'): + ans = f'8:5:{int(q[5:])}' + else: + if name.startswith('_'): + q = f'#{name[1:]}' + c = to_color(q) + if c is None: + raise AttributeError(f'{name} is not a valid color') + ans = f'8{color_as_sgr(c)}' + return f'\x1b[{self.which}{ans}m' + + +class PaneTitleFormatter: + reset = '\x1b[0m' + fg = PaneTitleColorFormatter('3') + bg = PaneTitleColorFormatter('4') + bold = '\x1b[1m' + nobold = '\x1b[22m' + italic = '\x1b[3m' + noitalic = '\x1b[23m' + + +def _draw_attributed_string(title: str, screen: Screen) -> None: + if '\x1b' in title: + for x in sgr_sanitizer_pat(for_splitting=True).split(title): + if x.startswith('\x1b') and x.endswith('m'): + screen.apply_sgr(x[2:-1]) + else: + screen.draw(x) + else: + screen.draw(title) + + +class PaneTitleData(NamedTuple): + title: str + is_active: bool + window_id: int + tab_id: int + + +class PaneTitleBarScreen: + def __init__(self, os_window_id: int, cell_width: int, cell_height: int): + self.os_window_id = os_window_id + self.cell_width = cell_width + self.screen = Screen(None, 1, 10, 0, cell_width, cell_height) + self.screen.reset_mode(DECAWM) + + def layout(self, geometry: WindowGeometry) -> None: + ncells = max(4, (geometry.right - geometry.left) // self.cell_width) + self.screen.resize(1, ncells) + self.geometry = geometry + + def render(self, data: PaneTitleData) -> None: + opts = get_options() + s = self.screen + s.cursor.x = 0 + s.erase_in_line(2, False) + + is_active = data.is_active + if is_active: + s.color_profile.default_fg = opts.pane_title_bar_active_fg + s.color_profile.default_bg = opts.pane_title_bar_active_bg + fg = (color_as_int(opts.pane_title_bar_active_fg) << 8) | 2 + bg = (color_as_int(opts.pane_title_bar_active_bg) << 8) | 2 + else: + s.color_profile.default_fg = opts.pane_title_bar_inactive_fg + s.color_profile.default_bg = opts.pane_title_bar_inactive_bg + fg = (color_as_int(opts.pane_title_bar_inactive_fg) << 8) | 2 + bg = (color_as_int(opts.pane_title_bar_inactive_bg) << 8) | 2 + + s.cursor.fg = fg + s.cursor.bg = bg + + template = opts.pane_title_template + if is_active and opts.active_pane_title_template and opts.active_pane_title_template != 'none': + template = opts.active_pane_title_template + + PaneTitleColorFormatter.is_active = is_active + eval_locals = { + 'title': data.title, + 'is_active': is_active, + 'fmt': PaneTitleFormatter, + } + try: + title = eval(_compile_template(template), {'__builtins__': safe_builtins}, eval_locals) + except Exception as e: + _report_template_failure(template, str(e)) + title = data.title + + title_str = str(title) + align = opts.pane_title_bar_align + + if align == 'left': + _draw_attributed_string(title_str, s) + else: + # Measure the title length by drawing to cursor position 0 + # and checking where the cursor ends up + _draw_attributed_string(title_str, s) + title_len = s.cursor.x + s.cursor.x = 0 + s.erase_in_line(2, False) + s.cursor.fg = fg + s.cursor.bg = bg + + if align == 'center': + pad = max(0, (s.columns - title_len) // 2) + else: # right + pad = max(0, s.columns - title_len) + + for _ in range(pad): + s.draw(' ') + _draw_attributed_string(title_str, s) + + # Fill remaining cells with background + while s.cursor.x < s.columns: + s.draw(' ') + + +class PaneTitleBarManager: + + def __init__(self, os_window_id: int, tab_id: int): + self.os_window_id = os_window_id + self.tab_id = tab_id + self._screens: dict[int, PaneTitleBarScreen] = {} + + def _clear_all(self) -> None: + for wid, pts in self._screens.items(): + # Zero geometry so the C render loop skips drawing + set_pane_title_bar_render_data( + self.os_window_id, self.tab_id, wid, pts.screen, + 0, 0, 0, 0, + ) + self._screens.clear() + + def update(self, all_windows: WindowList) -> None: + opts = get_options() + position = opts.pane_title_bar + if position == 'none': + if self._screens: + self._clear_all() + return + + visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) + if len(visible_groups) < 2: + if self._screens: + self._clear_all() + return + + cell_width, cell_height = cell_size_for_window(self.os_window_id) + active_group = all_windows.active_group + seen_window_ids: set[int] = set() + + for wg in visible_groups: + geom = wg.geometry + if geom is None: + continue + + window = wg.windows[-1] if wg.windows else None + if window is None: + continue + + # Validate geometry has enough space for a title bar + if geom.right <= geom.left or geom.bottom <= geom.top: + continue + if position == 'top' and geom.top < cell_height: + continue + if position == 'bottom' and geom.bottom + cell_height < geom.bottom: # overflow check + continue + + wid = window.id + seen_window_ids.add(wid) + + if wid not in self._screens: + self._screens[wid] = PaneTitleBarScreen(self.os_window_id, cell_width, cell_height) + + pts = self._screens[wid] + + # Calculate title bar geometry + if position == 'top': + title_geom = WindowGeometry( + left=geom.left, + top=geom.top - cell_height, + right=geom.right, + bottom=geom.top, + xnum=0, ynum=1, + ) + else: + title_geom = WindowGeometry( + left=geom.left, + top=geom.bottom, + right=geom.right, + bottom=geom.bottom + cell_height, + xnum=0, ynum=1, + ) + + pts.layout(title_geom) + + is_active = wg is active_group + data = PaneTitleData( + title=window.title or '', + is_active=is_active, + window_id=wid, + tab_id=self.tab_id, + ) + pts.render(data) + + set_pane_title_bar_render_data( + self.os_window_id, self.tab_id, wid, pts.screen, + title_geom.left, title_geom.top, title_geom.right, title_geom.bottom, + ) + + # Clean up screens for windows that are no longer visible + stale = set(self._screens) - seen_window_ids + for wid in stale: + del self._screens[wid] + + def destroy(self) -> None: + self._screens.clear() diff --git a/kitty/state.c b/kitty/state.c index e0e5fc0cb..e37512e4c 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -255,12 +255,16 @@ add_tab(id_type os_window_id) { static void create_gpu_resources_for_window(Window *w) { w->render_data.vao_idx = create_cell_vao(); + w->pane_title_render_data.vao_idx = create_cell_vao(); } static void release_gpu_resources_for_window(Window *w) { if (w->render_data.vao_idx > -1) remove_vao(w->render_data.vao_idx); w->render_data.vao_idx = -1; + if (w->pane_title_render_data.vao_idx > -1) remove_vao(w->pane_title_render_data.vao_idx); + w->pane_title_render_data.vao_idx = -1; + Py_CLEAR(w->pane_title_render_data.screen); } static bool @@ -855,6 +859,17 @@ PYWRAP1(set_tab_bar_render_data) { Py_RETURN_NONE; } +PYWRAP1(set_pane_title_bar_render_data) { + WindowGeometry g; + id_type os_window_id, tab_id, window_id; + Screen *screen; + PA("KKKOIIII", &os_window_id, &tab_id, &window_id, &screen, &g.left, &g.top, &g.right, &g.bottom); + WITH_WINDOW(os_window_id, tab_id, window_id) + init_window_render_data(&window->pane_title_render_data, g, screen); + END_WITH_WINDOW + Py_RETURN_NONE; +} + static PyTypeObject RegionType; static PyStructSequence_Field region_fields[] = { {"left", ""}, {"top", ""}, {"right", ""}, {"bottom", ""}, {"width", ""}, {"height", ""}, {NULL, NULL} @@ -1592,6 +1607,7 @@ static PyMethodDef module_methods[] = { MW(reorder_tabs, METH_VARARGS), MW(set_borders_rects, METH_VARARGS), MW(set_tab_bar_render_data, METH_VARARGS), + MW(set_pane_title_bar_render_data, METH_VARARGS), MW(set_window_render_data, METH_VARARGS), MW(set_window_padding, METH_VARARGS), MW(viewport_for_window, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 38c8a3cc9..05aec3b44 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -208,6 +208,7 @@ typedef struct Window { bool visible; PyObject *title; WindowRenderData render_data; + WindowRenderData pane_title_render_data; WindowLogoRenderData window_logo; MousePosition mouse_pos; struct { diff --git a/kitty/tabs.py b/kitty/tabs.py index 7ba37c54a..217f73cd2 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -15,6 +15,7 @@ from gettext import gettext as _ from typing import Any, Concatenate, Deque, NamedTuple, Optional, ParamSpec, TypeVar, cast from .borders import Border, Borders +from .pane_title_bar import PaneTitleBarManager from .child import Child from .cli_stub import CLIOptions, SaveAsSessionOptions from .constants import appname @@ -170,6 +171,7 @@ class Tab: # {{{ self.name = getattr(session_tab, 'name', '') self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or get_options().enabled_layouts] self.borders = Borders(self.os_window_id, self.id) + self.pane_title_bar_manager = PaneTitleBarManager(self.os_window_id, self.id) self.windows: WindowList = WindowList(self) self._last_used_layout: str | None = None self._current_layout_name: str | None = None @@ -440,6 +442,7 @@ class Tab: # {{{ self.mark_tab_bar_dirty() def title_changed(self, window: Window) -> None: + self.pane_title_bar_manager.update(self.windows) if window is self.active_window: tm = self.tab_manager_ref() if tm is not None: @@ -468,6 +471,7 @@ class Tab: # {{{ current_layout=ly, tab_bar_rects=tm.tab_bar_rects, draw_window_borders=draw_borders ) + self.pane_title_bar_manager.update(self.windows) def create_layout_object(self, name: str) -> Layout: return create_layout_object_for(name, self.os_window_id, self.id) diff --git a/tools/themes/collection.go b/tools/themes/collection.go index 6191c92e6..5ba3fb4d1 100644 --- a/tools/themes/collection.go +++ b/tools/themes/collection.go @@ -35,290 +35,294 @@ var _ = fmt.Print var AllColorSettingNames = map[string]bool{ // {{{ // generated by gen-config.py do not edit // ALL_COLORS_START - "active_border_color": true, - "active_tab_background": true, - "active_tab_foreground": true, - "background": true, - "bell_border_color": true, - "color0": true, - "color1": true, - "color10": true, - "color100": true, - "color101": true, - "color102": true, - "color103": true, - "color104": true, - "color105": true, - "color106": true, - "color107": true, - "color108": true, - "color109": true, - "color11": true, - "color110": true, - "color111": true, - "color112": true, - "color113": true, - "color114": true, - "color115": true, - "color116": true, - "color117": true, - "color118": true, - "color119": true, - "color12": true, - "color120": true, - "color121": true, - "color122": true, - "color123": true, - "color124": true, - "color125": true, - "color126": true, - "color127": true, - "color128": true, - "color129": true, - "color13": true, - "color130": true, - "color131": true, - "color132": true, - "color133": true, - "color134": true, - "color135": true, - "color136": true, - "color137": true, - "color138": true, - "color139": true, - "color14": true, - "color140": true, - "color141": true, - "color142": true, - "color143": true, - "color144": true, - "color145": true, - "color146": true, - "color147": true, - "color148": true, - "color149": true, - "color15": true, - "color150": true, - "color151": true, - "color152": true, - "color153": true, - "color154": true, - "color155": true, - "color156": true, - "color157": true, - "color158": true, - "color159": true, - "color16": true, - "color160": true, - "color161": true, - "color162": true, - "color163": true, - "color164": true, - "color165": true, - "color166": true, - "color167": true, - "color168": true, - "color169": true, - "color17": true, - "color170": true, - "color171": true, - "color172": true, - "color173": true, - "color174": true, - "color175": true, - "color176": true, - "color177": true, - "color178": true, - "color179": true, - "color18": true, - "color180": true, - "color181": true, - "color182": true, - "color183": true, - "color184": true, - "color185": true, - "color186": true, - "color187": true, - "color188": true, - "color189": true, - "color19": true, - "color190": true, - "color191": true, - "color192": true, - "color193": true, - "color194": true, - "color195": true, - "color196": true, - "color197": true, - "color198": true, - "color199": true, - "color2": true, - "color20": true, - "color200": true, - "color201": true, - "color202": true, - "color203": true, - "color204": true, - "color205": true, - "color206": true, - "color207": true, - "color208": true, - "color209": true, - "color21": true, - "color210": true, - "color211": true, - "color212": true, - "color213": true, - "color214": true, - "color215": true, - "color216": true, - "color217": true, - "color218": true, - "color219": true, - "color22": true, - "color220": true, - "color221": true, - "color222": true, - "color223": true, - "color224": true, - "color225": true, - "color226": true, - "color227": true, - "color228": true, - "color229": true, - "color23": true, - "color230": true, - "color231": true, - "color232": true, - "color233": true, - "color234": true, - "color235": true, - "color236": true, - "color237": true, - "color238": true, - "color239": true, - "color24": true, - "color240": true, - "color241": true, - "color242": true, - "color243": true, - "color244": true, - "color245": true, - "color246": true, - "color247": true, - "color248": true, - "color249": true, - "color25": true, - "color250": true, - "color251": true, - "color252": true, - "color253": true, - "color254": true, - "color255": true, - "color26": true, - "color27": true, - "color28": true, - "color29": true, - "color3": true, - "color30": true, - "color31": true, - "color32": true, - "color33": true, - "color34": true, - "color35": true, - "color36": true, - "color37": true, - "color38": true, - "color39": true, - "color4": true, - "color40": true, - "color41": true, - "color42": true, - "color43": true, - "color44": true, - "color45": true, - "color46": true, - "color47": true, - "color48": true, - "color49": true, - "color5": true, - "color50": true, - "color51": true, - "color52": true, - "color53": true, - "color54": true, - "color55": true, - "color56": true, - "color57": true, - "color58": true, - "color59": true, - "color6": true, - "color60": true, - "color61": true, - "color62": true, - "color63": true, - "color64": true, - "color65": true, - "color66": true, - "color67": true, - "color68": true, - "color69": true, - "color7": true, - "color70": true, - "color71": true, - "color72": true, - "color73": true, - "color74": true, - "color75": true, - "color76": true, - "color77": true, - "color78": true, - "color79": true, - "color8": true, - "color80": true, - "color81": true, - "color82": true, - "color83": true, - "color84": true, - "color85": true, - "color86": true, - "color87": true, - "color88": true, - "color89": true, - "color9": true, - "color90": true, - "color91": true, - "color92": true, - "color93": true, - "color94": true, - "color95": true, - "color96": true, - "color97": true, - "color98": true, - "color99": true, - "cursor": true, - "cursor_text_color": true, - "cursor_trail_color": true, - "foreground": true, - "inactive_border_color": true, - "inactive_tab_background": true, - "inactive_tab_foreground": true, - "macos_titlebar_color": true, - "mark1_background": true, - "mark1_foreground": true, - "mark2_background": true, - "mark2_foreground": true, - "mark3_background": true, - "mark3_foreground": true, - "scrollbar_handle_color": true, - "scrollbar_track_color": true, - "selection_background": true, - "selection_foreground": true, - "tab_bar_background": true, - "tab_bar_margin_color": true, - "url_color": true, - "visual_bell_color": true, - "wayland_titlebar_color": true, // ALL_COLORS_END + "active_border_color": true, + "active_tab_background": true, + "active_tab_foreground": true, + "background": true, + "bell_border_color": true, + "color0": true, + "color1": true, + "color10": true, + "color100": true, + "color101": true, + "color102": true, + "color103": true, + "color104": true, + "color105": true, + "color106": true, + "color107": true, + "color108": true, + "color109": true, + "color11": true, + "color110": true, + "color111": true, + "color112": true, + "color113": true, + "color114": true, + "color115": true, + "color116": true, + "color117": true, + "color118": true, + "color119": true, + "color12": true, + "color120": true, + "color121": true, + "color122": true, + "color123": true, + "color124": true, + "color125": true, + "color126": true, + "color127": true, + "color128": true, + "color129": true, + "color13": true, + "color130": true, + "color131": true, + "color132": true, + "color133": true, + "color134": true, + "color135": true, + "color136": true, + "color137": true, + "color138": true, + "color139": true, + "color14": true, + "color140": true, + "color141": true, + "color142": true, + "color143": true, + "color144": true, + "color145": true, + "color146": true, + "color147": true, + "color148": true, + "color149": true, + "color15": true, + "color150": true, + "color151": true, + "color152": true, + "color153": true, + "color154": true, + "color155": true, + "color156": true, + "color157": true, + "color158": true, + "color159": true, + "color16": true, + "color160": true, + "color161": true, + "color162": true, + "color163": true, + "color164": true, + "color165": true, + "color166": true, + "color167": true, + "color168": true, + "color169": true, + "color17": true, + "color170": true, + "color171": true, + "color172": true, + "color173": true, + "color174": true, + "color175": true, + "color176": true, + "color177": true, + "color178": true, + "color179": true, + "color18": true, + "color180": true, + "color181": true, + "color182": true, + "color183": true, + "color184": true, + "color185": true, + "color186": true, + "color187": true, + "color188": true, + "color189": true, + "color19": true, + "color190": true, + "color191": true, + "color192": true, + "color193": true, + "color194": true, + "color195": true, + "color196": true, + "color197": true, + "color198": true, + "color199": true, + "color2": true, + "color20": true, + "color200": true, + "color201": true, + "color202": true, + "color203": true, + "color204": true, + "color205": true, + "color206": true, + "color207": true, + "color208": true, + "color209": true, + "color21": true, + "color210": true, + "color211": true, + "color212": true, + "color213": true, + "color214": true, + "color215": true, + "color216": true, + "color217": true, + "color218": true, + "color219": true, + "color22": true, + "color220": true, + "color221": true, + "color222": true, + "color223": true, + "color224": true, + "color225": true, + "color226": true, + "color227": true, + "color228": true, + "color229": true, + "color23": true, + "color230": true, + "color231": true, + "color232": true, + "color233": true, + "color234": true, + "color235": true, + "color236": true, + "color237": true, + "color238": true, + "color239": true, + "color24": true, + "color240": true, + "color241": true, + "color242": true, + "color243": true, + "color244": true, + "color245": true, + "color246": true, + "color247": true, + "color248": true, + "color249": true, + "color25": true, + "color250": true, + "color251": true, + "color252": true, + "color253": true, + "color254": true, + "color255": true, + "color26": true, + "color27": true, + "color28": true, + "color29": true, + "color3": true, + "color30": true, + "color31": true, + "color32": true, + "color33": true, + "color34": true, + "color35": true, + "color36": true, + "color37": true, + "color38": true, + "color39": true, + "color4": true, + "color40": true, + "color41": true, + "color42": true, + "color43": true, + "color44": true, + "color45": true, + "color46": true, + "color47": true, + "color48": true, + "color49": true, + "color5": true, + "color50": true, + "color51": true, + "color52": true, + "color53": true, + "color54": true, + "color55": true, + "color56": true, + "color57": true, + "color58": true, + "color59": true, + "color6": true, + "color60": true, + "color61": true, + "color62": true, + "color63": true, + "color64": true, + "color65": true, + "color66": true, + "color67": true, + "color68": true, + "color69": true, + "color7": true, + "color70": true, + "color71": true, + "color72": true, + "color73": true, + "color74": true, + "color75": true, + "color76": true, + "color77": true, + "color78": true, + "color79": true, + "color8": true, + "color80": true, + "color81": true, + "color82": true, + "color83": true, + "color84": true, + "color85": true, + "color86": true, + "color87": true, + "color88": true, + "color89": true, + "color9": true, + "color90": true, + "color91": true, + "color92": true, + "color93": true, + "color94": true, + "color95": true, + "color96": true, + "color97": true, + "color98": true, + "color99": true, + "cursor": true, + "cursor_text_color": true, + "cursor_trail_color": true, + "foreground": true, + "inactive_border_color": true, + "inactive_tab_background": true, + "inactive_tab_foreground": true, + "macos_titlebar_color": true, + "mark1_background": true, + "mark1_foreground": true, + "mark2_background": true, + "mark2_foreground": true, + "mark3_background": true, + "mark3_foreground": true, + "pane_title_bar_active_bg": true, + "pane_title_bar_active_fg": true, + "pane_title_bar_inactive_bg": true, + "pane_title_bar_inactive_fg": true, + "scrollbar_handle_color": true, + "scrollbar_track_color": true, + "selection_background": true, + "selection_foreground": true, + "tab_bar_background": true, + "tab_bar_margin_color": true, + "url_color": true, + "visual_bell_color": true, + "wayland_titlebar_color": true, // ALL_COLORS_END } // }}} type JSONMetadata struct { From cc32af250b5c1236e446071fc235324bdcff8e4e Mon Sep 17 00:00:00 2001 From: mcrmck Date: Sun, 1 Feb 2026 11:00:06 -0500 Subject: [PATCH 2/5] =?UTF-8?q?Rename=20pane=20=E2=86=92=20window=20title?= =?UTF-8?q?=20bar=20per=20reviewer=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename all options from pane_title_* to window_title_* - Use foreground/background instead of fg/bg in color option names - Change color options to to_color_or_none defaulting to None, falling back to corresponding tab bar colors - Add bell_symbol, activity_symbol, progress_percent template vars using existing bell_on_tab and tab_activity_symbol options - Add custom script support via window_title_bar.py in config dir (draw_window_title function exposed as {custom} in templates) - Update C structs, Python references, and regenerate config files Co-Authored-By: Claude Opus 4.5 --- kitty/child-monitor.c | 18 +- kitty/fast_data_types.pyi | 2 +- kitty/layout/base.py | 6 +- kitty/options/definition.py | 60 +- kitty/options/parse.py | 70 +-- kitty/options/to-c-generated.h | 60 ++ kitty/options/types.py | 42 +- kitty/state.c | 14 +- kitty/state.h | 5 +- kitty/tabs.py | 8 +- ...{pane_title_bar.py => window_title_bar.py} | 146 +++-- tools/cmd/at/set_colors.go | 22 +- tools/themes/collection.go | 576 +++++++++--------- 13 files changed, 593 insertions(+), 436 deletions(-) rename kitty/{pane_title_bar.py => window_title_bar.py} (58%) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 6d52d970f..d1afce3bd 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -807,11 +807,11 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * if (send_cell_data_to_gpu(WD.vao_idx, WD.screen, os_window)) needs_render = true; if (WD.screen->start_visual_bell_at != 0) needs_render = true; } - // Prepare pane title bar screen data for GPU - if (w->visible && w->pane_title_render_data.screen) { - CursorRenderInfo *cri = &w->pane_title_render_data.screen->cursor_render_info; + // Prepare window title bar screen data for GPU + if (w->visible && w->window_title_render_data.screen) { + CursorRenderInfo *cri = &w->window_title_render_data.screen->cursor_render_info; zero_at_ptr(cri); - if (send_cell_data_to_gpu(w->pane_title_render_data.vao_idx, w->pane_title_render_data.screen, os_window)) needs_render = true; + if (send_cell_data_to_gpu(w->window_title_render_data.vao_idx, w->window_title_render_data.screen, os_window)) needs_render = true; } } return needs_render || was_previously_rendered_with_layers != os_window->needs_layers; @@ -874,13 +874,13 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co if (WD.screen->start_visual_bell_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); } } - // Draw pane title bars + // Draw window title bars for (unsigned int i = 0; i < tab->num_windows; i++) { Window *w = tab->windows + i; - if (w->visible && w->pane_title_render_data.screen && - w->pane_title_render_data.geometry.right > w->pane_title_render_data.geometry.left && - w->pane_title_render_data.geometry.bottom > w->pane_title_render_data.geometry.top) { - draw_cells(&w->pane_title_render_data, os_window, i == tab->active_window, true, false, NULL); + if (w->visible && w->window_title_render_data.screen && + w->window_title_render_data.geometry.right > w->window_title_render_data.geometry.left && + w->window_title_render_data.geometry.bottom > w->window_title_render_data.geometry.top) { + draw_cells(&w->window_title_render_data, os_window, i == tab->active_window, true, false, NULL); } } setup_os_window_for_rendering(os_window, tab, active_window, false); diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index c3a0eec68..13fec807c 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1404,7 +1404,7 @@ def set_tab_bar_render_data( pass -def set_pane_title_bar_render_data( +def set_window_title_bar_render_data( os_window_id: int, tab_id: int, window_id: int, screen: Screen, left: int, top: int, right: int, bottom: int ) -> None: diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 667fdfdf9..3c7f3a72a 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -368,11 +368,11 @@ class Layout: self.update_visibility(all_windows) self.blank_rects = [] self.do_layout(all_windows) - self._apply_pane_title_bars(all_windows) + self._apply_window_title_bars(all_windows) - def _apply_pane_title_bars(self, all_windows: WindowList) -> None: + def _apply_window_title_bars(self, all_windows: WindowList) -> None: opts = get_options() - position = opts.pane_title_bar + position = opts.window_title_bar if position == 'none': return visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 409e619de..6cfed2f66 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1467,57 +1467,71 @@ actually resize the window itself, but instead, the layout row/column/slot, whic in multiple windows getting resized. ''') -opt('pane_title_bar', 'none', +opt('window_title_bar', 'none', choices=('none', 'top', 'bottom'), long_text=''' -Show a title bar for each window/pane when there are multiple windows in a tab. +Show a title bar for each window when there are multiple windows in a tab. The title bar displays the window title and is hidden when only a single window is visible. The value controls the position of the title bar relative to the window content. Set to :code:`none` to disable. ''' ) -opt('pane_title_template', '"{title}"', +opt('window_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}"', option_type='tab_title_template', long_text=''' -A template to render the pane title bar text. Uses the same template syntax as +A template to render the window title bar text. Uses the same template syntax as :opt:`tab_title_template`. Available variables include: :code:`{title}`, -:code:`{index}`, :code:`{layout_name}`, :code:`{num_windows}`, -:code:`{num_window_groups}`, :code:`{tab.active_wd}`, etc. +:code:`{bell_symbol}`, :code:`{activity_symbol}`, :code:`{progress_percent}`, +:code:`{custom}`, :code:`{fmt}`, :code:`{is_active}`. +You can also provide a custom :code:`draw_window_title(data)` function in +:file:`window_title_bar.py` in the kitty config directory, exposed as :code:`{custom}`. ''' ) -opt('active_pane_title_template', 'none', +opt('active_window_title_template', 'none', option_type='tab_title_template', long_text=''' -Template to use for the active pane title bar. If not set (the value -:code:`none`), the :opt:`pane_title_template` is used. +Template to use for the active window title bar. If not set (the value +:code:`none`), the :opt:`window_title_template` is used. ''' ) -opt('pane_title_bar_active_fg', '#000000', - option_type='to_color', - long_text='Foreground color for the active pane title bar.' +opt('window_title_bar_active_foreground', 'none', + option_type='to_color_or_none', ctype='color_or_none_as_int', + long_text=''' +Foreground color for the active window title bar. Defaults to +:opt:`active_tab_foreground` when set to :code:`none`. +''' ) -opt('pane_title_bar_active_bg', '#00ff00', - option_type='to_color', - long_text='Background color for the active pane title bar.' +opt('window_title_bar_active_background', 'none', + option_type='to_color_or_none', ctype='color_or_none_as_int', + long_text=''' +Background color for the active window title bar. Defaults to +:opt:`active_tab_background` when set to :code:`none`. +''' ) -opt('pane_title_bar_inactive_fg', '#cccccc', - option_type='to_color', - long_text='Foreground color for inactive pane title bars.' +opt('window_title_bar_inactive_foreground', 'none', + option_type='to_color_or_none', ctype='color_or_none_as_int', + long_text=''' +Foreground color for inactive window title bars. Defaults to +:opt:`inactive_tab_foreground` when set to :code:`none`. +''' ) -opt('pane_title_bar_inactive_bg', '#333333', - option_type='to_color', - long_text='Background color for inactive pane title bars.' +opt('window_title_bar_inactive_background', 'none', + option_type='to_color_or_none', ctype='color_or_none_as_int', + long_text=''' +Background color for inactive window title bars. Defaults to +:opt:`inactive_tab_background` when set to :code:`none`. +''' ) -opt('pane_title_bar_align', 'center', +opt('window_title_bar_align', 'center', choices=('left', 'center', 'right'), - long_text='Horizontal alignment of the text in pane title bars.' + long_text='Horizontal alignment of the text in window title bars.' ) egr() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 8f8249ba3..2bf282acb 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -36,9 +36,6 @@ class Parser: def active_border_color(self, val: str, ans: dict[str, typing.Any]) -> None: ans['active_border_color'] = to_color_or_none(val) - def active_pane_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['active_pane_title_template'] = tab_title_template(val) - def active_tab_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['active_tab_background'] = to_color(val) @@ -51,6 +48,9 @@ class Parser: def active_tab_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: ans['active_tab_title_template'] = active_tab_title_template(val) + def active_window_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['active_window_title_template'] = tab_title_template(val) + def allow_cloning(self, val: str, ans: dict[str, typing.Any]) -> None: val = val.lower() if val not in self.choices_for_allow_cloning: @@ -1168,37 +1168,6 @@ class Parser: def open_url_with(self, val: str, ans: dict[str, typing.Any]) -> None: ans['open_url_with'] = to_cmdline(val) - def pane_title_bar(self, val: str, ans: dict[str, typing.Any]) -> None: - val = val.lower() - if val not in self.choices_for_pane_title_bar: - raise ValueError(f"The value {val} is not a valid choice for pane_title_bar") - ans["pane_title_bar"] = val - - choices_for_pane_title_bar = frozenset(('none', 'top', 'bottom')) - - def pane_title_bar_active_bg(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['pane_title_bar_active_bg'] = to_color(val) - - def pane_title_bar_active_fg(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['pane_title_bar_active_fg'] = to_color(val) - - def pane_title_bar_align(self, val: str, ans: dict[str, typing.Any]) -> None: - val = val.lower() - if val not in self.choices_for_pane_title_bar_align: - raise ValueError(f"The value {val} is not a valid choice for pane_title_bar_align") - ans["pane_title_bar_align"] = val - - choices_for_pane_title_bar_align = frozenset(('left', 'center', 'right')) - - def pane_title_bar_inactive_bg(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['pane_title_bar_inactive_bg'] = to_color(val) - - def pane_title_bar_inactive_fg(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['pane_title_bar_inactive_fg'] = to_color(val) - - def pane_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['pane_title_template'] = tab_title_template(val) - def paste_actions(self, val: str, ans: dict[str, typing.Any]) -> None: ans['paste_actions'] = paste_actions(val) @@ -1356,7 +1325,7 @@ class Parser: raise ValueError(f"The value {val} is not a valid choice for tab_bar_align") ans["tab_bar_align"] = val - choices_for_tab_bar_align = choices_for_pane_title_bar_align + choices_for_tab_bar_align = frozenset(('left', 'center', 'right')) def tab_bar_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['tab_bar_background'] = to_color_or_none(val) @@ -1538,6 +1507,37 @@ class Parser: def window_resize_step_lines(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_resize_step_lines'] = positive_int(val) + def window_title_bar(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_window_title_bar: + raise ValueError(f"The value {val} is not a valid choice for window_title_bar") + ans["window_title_bar"] = val + + choices_for_window_title_bar = frozenset(('none', 'top', 'bottom')) + + def window_title_bar_active_background(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_active_background'] = to_color_or_none(val) + + def window_title_bar_active_foreground(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_active_foreground'] = to_color_or_none(val) + + def window_title_bar_align(self, val: str, ans: dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_window_title_bar_align: + raise ValueError(f"The value {val} is not a valid choice for window_title_bar_align") + ans["window_title_bar_align"] = val + + choices_for_window_title_bar_align = choices_for_tab_bar_align + + def window_title_bar_inactive_background(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_inactive_background'] = to_color_or_none(val) + + def window_title_bar_inactive_foreground(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_inactive_foreground'] = to_color_or_none(val) + + def window_title_template(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_template'] = tab_title_template(val) + def x11_hide_window_decorations(self, val: str, ans: dict[str, typing.Any]) -> None: deprecated_hide_window_decorations_aliases('x11_hide_window_decorations', val, ans) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 72d5637fa..5a36c5415 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -993,6 +993,58 @@ convert_from_opts_window_drag_tolerance(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_window_title_bar_active_foreground(PyObject *val, Options *opts) { + opts->window_title_bar_active_foreground = color_or_none_as_int(val); +} + +static void +convert_from_opts_window_title_bar_active_foreground(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_active_foreground"); + if (ret == NULL) return; + convert_from_python_window_title_bar_active_foreground(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_window_title_bar_active_background(PyObject *val, Options *opts) { + opts->window_title_bar_active_background = color_or_none_as_int(val); +} + +static void +convert_from_opts_window_title_bar_active_background(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_active_background"); + if (ret == NULL) return; + convert_from_python_window_title_bar_active_background(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_window_title_bar_inactive_foreground(PyObject *val, Options *opts) { + opts->window_title_bar_inactive_foreground = color_or_none_as_int(val); +} + +static void +convert_from_opts_window_title_bar_inactive_foreground(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_inactive_foreground"); + if (ret == NULL) return; + convert_from_python_window_title_bar_inactive_foreground(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_window_title_bar_inactive_background(PyObject *val, Options *opts) { + opts->window_title_bar_inactive_background = color_or_none_as_int(val); +} + +static void +convert_from_opts_window_title_bar_inactive_background(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "window_title_bar_inactive_background"); + if (ret == NULL) return; + convert_from_python_window_title_bar_inactive_background(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_tab_bar_edge(PyObject *val, Options *opts) { opts->tab_bar_edge = PyLong_AsLong(val); @@ -1550,6 +1602,14 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_window_drag_tolerance(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_window_title_bar_active_foreground(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_window_title_bar_active_background(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_window_title_bar_inactive_foreground(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_window_title_bar_inactive_background(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_edge(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_margin_height(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index a6711a79a..8cf5bb2d6 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -25,13 +25,11 @@ choices_for_default_pointer_shape = typing.Literal['arrow', 'beam', 'text', 'poi choices_for_linux_display_server = typing.Literal['auto', 'wayland', 'x11'] choices_for_macos_colorspace = typing.Literal['srgb', 'default', 'displayp3'] choices_for_macos_show_window_title_in = typing.Literal['all', 'menubar', 'none', 'window'] -choices_for_pane_title_bar = typing.Literal['none', 'top', 'bottom'] -choices_for_pane_title_bar_align = typing.Literal['left', 'center', 'right'] choices_for_placement_strategy = typing.Literal['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'] choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] -choices_for_tab_bar_align = choices_for_pane_title_bar_align +choices_for_tab_bar_align = typing.Literal['left', 'center', 'right'] choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separator', 'slant', 'custom'] choices_for_tab_powerline_style = typing.Literal['angled', 'round', 'slanted'] choices_for_tab_switch_strategy = typing.Literal['last', 'left', 'previous', 'right'] @@ -39,15 +37,17 @@ choices_for_terminfo_type = typing.Literal['path', 'direct', 'none'] choices_for_undercurl_style = typing.Literal['thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense'] choices_for_underline_hyperlinks = typing.Literal['hover', 'always', 'never'] choices_for_window_logo_position = choices_for_placement_strategy +choices_for_window_title_bar = typing.Literal['none', 'top', 'bottom'] +choices_for_window_title_bar_align = choices_for_tab_bar_align option_names = ( 'action_alias', 'active_border_color', - 'active_pane_title_template', 'active_tab_background', 'active_tab_font_style', 'active_tab_foreground', 'active_tab_title_template', + 'active_window_title_template', 'allow_cloning', 'allow_hyperlinks', 'allow_remote_control', @@ -408,13 +408,6 @@ option_names = ( 'narrow_symbols', 'notify_on_cmd_finish', 'open_url_with', - 'pane_title_bar', - 'pane_title_bar_active_bg', - 'pane_title_bar_active_fg', - 'pane_title_bar_align', - 'pane_title_bar_inactive_bg', - 'pane_title_bar_inactive_fg', - 'pane_title_template', 'paste_actions', 'pixel_scroll', 'placement_strategy', @@ -507,16 +500,23 @@ option_names = ( 'window_padding_width', 'window_resize_step_cells', 'window_resize_step_lines', + 'window_title_bar', + 'window_title_bar_active_background', + 'window_title_bar_active_foreground', + 'window_title_bar_align', + 'window_title_bar_inactive_background', + 'window_title_bar_inactive_foreground', + 'window_title_template', ) class Options: active_border_color: kitty.fast_data_types.Color | None = Color(0, 255, 0) - active_pane_title_template: str = 'none' active_tab_background: Color = Color(238, 238, 238) active_tab_font_style: tuple[bool, bool] = (True, True) active_tab_foreground: Color = Color(0, 0, 0) active_tab_title_template: str | None = None + active_window_title_template: str = 'none' allow_cloning: choices_for_allow_cloning = 'ask' allow_hyperlinks: int = 1 allow_remote_control: choices_for_allow_remote_control = 'no' @@ -611,13 +611,6 @@ class Options: mouse_hide_wait: MouseHideWait = MouseHideWait(hide_wait=0.0, show_wait=0.0, show_threshold=40, scroll_show=True) if is_macos else MouseHideWait(hide_wait=3.0, show_wait=0.0, show_threshold=40, scroll_show=True) notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=(), clear_on=('focus', 'next')) open_url_with: list[str] = ['default'] - pane_title_bar: choices_for_pane_title_bar = 'none' - pane_title_bar_active_bg: Color = Color(0, 255, 0) - pane_title_bar_active_fg: Color = Color(0, 0, 0) - pane_title_bar_align: choices_for_pane_title_bar_align = 'center' - pane_title_bar_inactive_bg: Color = Color(51, 51, 51) - pane_title_bar_inactive_fg: Color = Color(204, 204, 204) - pane_title_template: str = '{title}' paste_actions: frozenset[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) pixel_scroll: bool = True placement_strategy: choices_for_placement_strategy = 'center' @@ -707,6 +700,13 @@ class Options: window_padding_width: FloatEdges = FloatEdges(left=0, top=0, right=0, bottom=0) window_resize_step_cells: int = 2 window_resize_step_lines: int = 2 + window_title_bar: choices_for_window_title_bar = 'none' + window_title_bar_active_background: kitty.fast_data_types.Color | None = None + window_title_bar_active_foreground: kitty.fast_data_types.Color | None = None + window_title_bar_align: choices_for_window_title_bar_align = 'center' + window_title_bar_inactive_background: kitty.fast_data_types.Color | None = None + window_title_bar_inactive_foreground: kitty.fast_data_types.Color | None = None + window_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}' action_alias: dict[str, str] = {} env: dict[str, str] = {} exe_search_path: dict[str, str] = {} @@ -1127,6 +1127,10 @@ nullable_colors = frozenset({ 'cursor_trail_color', 'visual_bell_color', 'active_border_color', + 'window_title_bar_active_foreground', + 'window_title_bar_active_background', + 'window_title_bar_inactive_foreground', + 'window_title_bar_inactive_background', 'tab_bar_background', 'tab_bar_margin_color', 'selection_foreground', diff --git a/kitty/state.c b/kitty/state.c index e37512e4c..c35c68f5d 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -255,16 +255,16 @@ add_tab(id_type os_window_id) { static void create_gpu_resources_for_window(Window *w) { w->render_data.vao_idx = create_cell_vao(); - w->pane_title_render_data.vao_idx = create_cell_vao(); + w->window_title_render_data.vao_idx = create_cell_vao(); } static void release_gpu_resources_for_window(Window *w) { if (w->render_data.vao_idx > -1) remove_vao(w->render_data.vao_idx); w->render_data.vao_idx = -1; - if (w->pane_title_render_data.vao_idx > -1) remove_vao(w->pane_title_render_data.vao_idx); - w->pane_title_render_data.vao_idx = -1; - Py_CLEAR(w->pane_title_render_data.screen); + if (w->window_title_render_data.vao_idx > -1) remove_vao(w->window_title_render_data.vao_idx); + w->window_title_render_data.vao_idx = -1; + Py_CLEAR(w->window_title_render_data.screen); } static bool @@ -859,13 +859,13 @@ PYWRAP1(set_tab_bar_render_data) { Py_RETURN_NONE; } -PYWRAP1(set_pane_title_bar_render_data) { +PYWRAP1(set_window_title_bar_render_data) { WindowGeometry g; id_type os_window_id, tab_id, window_id; Screen *screen; PA("KKKOIIII", &os_window_id, &tab_id, &window_id, &screen, &g.left, &g.top, &g.right, &g.bottom); WITH_WINDOW(os_window_id, tab_id, window_id) - init_window_render_data(&window->pane_title_render_data, g, screen); + init_window_render_data(&window->window_title_render_data, g, screen); END_WITH_WINDOW Py_RETURN_NONE; } @@ -1607,7 +1607,7 @@ static PyMethodDef module_methods[] = { MW(reorder_tabs, METH_VARARGS), MW(set_borders_rects, METH_VARARGS), MW(set_tab_bar_render_data, METH_VARARGS), - MW(set_pane_title_bar_render_data, METH_VARARGS), + MW(set_window_title_bar_render_data, METH_VARARGS), MW(set_window_render_data, METH_VARARGS), MW(set_window_padding, METH_VARARGS), MW(viewport_for_window, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 05aec3b44..a0409d090 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -62,7 +62,8 @@ typedef struct Options { bool scrollback_fill_enlarged_window; char_type *select_by_word_characters; char_type *select_by_word_characters_forward; - color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color, tab_bar_background, tab_bar_margin_color; + color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color, tab_bar_background, tab_bar_margin_color, + window_title_bar_active_foreground, window_title_bar_active_background, window_title_bar_inactive_foreground, window_title_bar_inactive_background; monotonic_t repaint_delay, input_delay; bool focus_follows_mouse; unsigned int hide_window_decorations; @@ -208,7 +209,7 @@ typedef struct Window { bool visible; PyObject *title; WindowRenderData render_data; - WindowRenderData pane_title_render_data; + WindowRenderData window_title_render_data; WindowLogoRenderData window_logo; MousePosition mouse_pos; struct { diff --git a/kitty/tabs.py b/kitty/tabs.py index 217f73cd2..456f38a17 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -15,7 +15,7 @@ from gettext import gettext as _ from typing import Any, Concatenate, Deque, NamedTuple, Optional, ParamSpec, TypeVar, cast from .borders import Border, Borders -from .pane_title_bar import PaneTitleBarManager +from .window_title_bar import WindowTitleBarManager from .child import Child from .cli_stub import CLIOptions, SaveAsSessionOptions from .constants import appname @@ -171,7 +171,7 @@ class Tab: # {{{ self.name = getattr(session_tab, 'name', '') self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or get_options().enabled_layouts] self.borders = Borders(self.os_window_id, self.id) - self.pane_title_bar_manager = PaneTitleBarManager(self.os_window_id, self.id) + self.window_title_bar_manager = WindowTitleBarManager(self.os_window_id, self.id) self.windows: WindowList = WindowList(self) self._last_used_layout: str | None = None self._current_layout_name: str | None = None @@ -442,7 +442,7 @@ class Tab: # {{{ self.mark_tab_bar_dirty() def title_changed(self, window: Window) -> None: - self.pane_title_bar_manager.update(self.windows) + self.window_title_bar_manager.update(self.windows) if window is self.active_window: tm = self.tab_manager_ref() if tm is not None: @@ -471,7 +471,7 @@ class Tab: # {{{ current_layout=ly, tab_bar_rects=tm.tab_bar_rects, draw_window_borders=draw_borders ) - self.pane_title_bar_manager.update(self.windows) + self.window_title_bar_manager.update(self.windows) def create_layout_object(self, name: str) -> Layout: return create_layout_object_for(name, self.os_window_id, self.id) diff --git a/kitty/pane_title_bar.py b/kitty/window_title_bar.py similarity index 58% rename from kitty/pane_title_bar.py rename to kitty/window_title_bar.py index c3953af62..fcc53a4bb 100644 --- a/kitty/pane_title_bar.py +++ b/kitty/window_title_bar.py @@ -1,31 +1,34 @@ #!/usr/bin/env python # License: GPL v3 Copyright: 2024, kitty contributors +import os from functools import lru_cache from typing import Any, NamedTuple +from .constants import config_dir from .fast_data_types import ( DECAWM, Screen, cell_size_for_window, get_options, - set_pane_title_bar_render_data, + set_window_title_bar_render_data, ) +from .progress import ProgressState from .rgb import color_as_sgr, color_from_int, to_color -from .types import WindowGeometry +from .types import WindowGeometry, run_once from .utils import color_as_int, log_error, sgr_sanitizer_pat from .window_list import WindowList @lru_cache def _report_template_failure(template: str, e: str) -> None: - log_error(f'Invalid pane title template: "{template}" with error: {e}') + log_error(f'Invalid window title template: "{template}" with error: {e}') @lru_cache def _compile_template(template: str) -> Any: try: - return compile('f"""' + template + '"""', '', 'eval') + return compile('f"""' + template + '"""', '', 'eval') except Exception as e: _report_template_failure(template, str(e)) @@ -36,7 +39,13 @@ safe_builtins = { } -class PaneTitleColorFormatter: +def _resolve_color(opt_val: Any, fallback_val: Any) -> Any: + if opt_val is None: + return fallback_val + return opt_val + + +class WindowTitleColorFormatter: is_active: bool = False def __init__(self, which: str): @@ -46,12 +55,16 @@ class PaneTitleColorFormatter: q = name if q == 'default': ans = '9' - elif q == 'pane': + elif q == 'window': opts = get_options() if self.is_active: - col = color_from_int(color_as_int(opts.pane_title_bar_active_fg if self.which == '3' else opts.pane_title_bar_active_bg)) + fg_color = _resolve_color(opts.window_title_bar_active_foreground, opts.active_tab_foreground) + bg_color = _resolve_color(opts.window_title_bar_active_background, opts.active_tab_background) + col = color_from_int(color_as_int(fg_color if self.which == '3' else bg_color)) else: - col = color_from_int(color_as_int(opts.pane_title_bar_inactive_fg if self.which == '3' else opts.pane_title_bar_inactive_bg)) + fg_color = _resolve_color(opts.window_title_bar_inactive_foreground, opts.inactive_tab_foreground) + bg_color = _resolve_color(opts.window_title_bar_inactive_background, opts.inactive_tab_background) + col = color_from_int(color_as_int(fg_color if self.which == '3' else bg_color)) ans = f'8{color_as_sgr(col)}' elif q.startswith('color'): ans = f'8:5:{int(q[5:])}' @@ -65,10 +78,10 @@ class PaneTitleColorFormatter: return f'\x1b[{self.which}{ans}m' -class PaneTitleFormatter: +class WindowTitleFormatter: reset = '\x1b[0m' - fg = PaneTitleColorFormatter('3') - bg = PaneTitleColorFormatter('4') + fg = WindowTitleColorFormatter('3') + bg = WindowTitleColorFormatter('4') bold = '\x1b[1m' nobold = '\x1b[22m' italic = '\x1b[3m' @@ -86,14 +99,46 @@ def _draw_attributed_string(title: str, screen: Screen) -> None: screen.draw(title) -class PaneTitleData(NamedTuple): +class WindowTitleData(NamedTuple): title: str is_active: bool window_id: int tab_id: int + needs_attention: bool = False + has_activity_since_last_focus: bool = False -class PaneTitleBarScreen: +@run_once +def load_custom_window_title_bar_module() -> dict[str, Any]: + import runpy + import traceback + try: + return runpy.run_path(os.path.join(config_dir, 'window_title_bar.py')) + except FileNotFoundError: + return {} + except Exception as e: + traceback.print_exc() + log_error(f'Failed to load custom window_title_bar.py module with error: {e}') + return {} + + +def _get_custom_draw_result(data: WindowTitleData) -> str | None: + m = load_custom_window_title_bar_module() + func = m.get('draw_window_title') + if func is None: + return None + try: + return str(func(data)) + except Exception as e: + log_error(f'Custom draw_window_title function failed with error: {e}') + return None + + +def clear_caches() -> None: + load_custom_window_title_bar_module.clear_cached() + + +class WindowTitleBarScreen: def __init__(self, os_window_id: int, cell_width: int, cell_height: int): self.os_window_id = os_window_id self.cell_width = cell_width @@ -105,7 +150,7 @@ class PaneTitleBarScreen: self.screen.resize(1, ncells) self.geometry = geometry - def render(self, data: PaneTitleData) -> None: + def render(self, data: WindowTitleData, progress_percent: str) -> None: opts = get_options() s = self.screen s.cursor.x = 0 @@ -113,28 +158,39 @@ class PaneTitleBarScreen: is_active = data.is_active if is_active: - s.color_profile.default_fg = opts.pane_title_bar_active_fg - s.color_profile.default_bg = opts.pane_title_bar_active_bg - fg = (color_as_int(opts.pane_title_bar_active_fg) << 8) | 2 - bg = (color_as_int(opts.pane_title_bar_active_bg) << 8) | 2 + fg_color = _resolve_color(opts.window_title_bar_active_foreground, opts.active_tab_foreground) + bg_color = _resolve_color(opts.window_title_bar_active_background, opts.active_tab_background) else: - s.color_profile.default_fg = opts.pane_title_bar_inactive_fg - s.color_profile.default_bg = opts.pane_title_bar_inactive_bg - fg = (color_as_int(opts.pane_title_bar_inactive_fg) << 8) | 2 - bg = (color_as_int(opts.pane_title_bar_inactive_bg) << 8) | 2 + fg_color = _resolve_color(opts.window_title_bar_inactive_foreground, opts.inactive_tab_foreground) + bg_color = _resolve_color(opts.window_title_bar_inactive_background, opts.inactive_tab_background) + + s.color_profile.default_fg = fg_color + s.color_profile.default_bg = bg_color + fg = (color_as_int(fg_color) << 8) | 2 + bg = (color_as_int(bg_color) << 8) | 2 s.cursor.fg = fg s.cursor.bg = bg - template = opts.pane_title_template - if is_active and opts.active_pane_title_template and opts.active_pane_title_template != 'none': - template = opts.active_pane_title_template + template = opts.window_title_template + if is_active and opts.active_window_title_template and opts.active_window_title_template != 'none': + template = opts.active_window_title_template + + WindowTitleColorFormatter.is_active = is_active + + bell_symbol = opts.bell_on_tab if data.needs_attention else '' + activity_symbol = opts.tab_activity_symbol if data.has_activity_since_last_focus else '' + + custom_result = _get_custom_draw_result(data) - PaneTitleColorFormatter.is_active = is_active eval_locals = { 'title': data.title, 'is_active': is_active, - 'fmt': PaneTitleFormatter, + 'fmt': WindowTitleFormatter, + 'bell_symbol': bell_symbol, + 'activity_symbol': activity_symbol, + 'progress_percent': progress_percent, + 'custom': custom_result or '', } try: title = eval(_compile_template(template), {'__builtins__': safe_builtins}, eval_locals) @@ -143,7 +199,7 @@ class PaneTitleBarScreen: title = data.title title_str = str(title) - align = opts.pane_title_bar_align + align = opts.window_title_bar_align if align == 'left': _draw_attributed_string(title_str, s) @@ -171,17 +227,17 @@ class PaneTitleBarScreen: s.draw(' ') -class PaneTitleBarManager: +class WindowTitleBarManager: def __init__(self, os_window_id: int, tab_id: int): self.os_window_id = os_window_id self.tab_id = tab_id - self._screens: dict[int, PaneTitleBarScreen] = {} + self._screens: dict[int, WindowTitleBarScreen] = {} def _clear_all(self) -> None: for wid, pts in self._screens.items(): # Zero geometry so the C render loop skips drawing - set_pane_title_bar_render_data( + set_window_title_bar_render_data( self.os_window_id, self.tab_id, wid, pts.screen, 0, 0, 0, 0, ) @@ -189,7 +245,7 @@ class PaneTitleBarManager: def update(self, all_windows: WindowList) -> None: opts = get_options() - position = opts.pane_title_bar + position = opts.window_title_bar if position == 'none': if self._screens: self._clear_all() @@ -226,7 +282,7 @@ class PaneTitleBarManager: seen_window_ids.add(wid) if wid not in self._screens: - self._screens[wid] = PaneTitleBarScreen(self.os_window_id, cell_width, cell_height) + self._screens[wid] = WindowTitleBarScreen(self.os_window_id, cell_width, cell_height) pts = self._screens[wid] @@ -251,15 +307,33 @@ class PaneTitleBarManager: pts.layout(title_geom) is_active = wg is active_group - data = PaneTitleData( + + # Get bell/activity state from the window object + needs_attention = getattr(window, 'needs_attention', False) + has_activity = getattr(window, 'has_activity_since_last_focus', False) + if callable(has_activity): + has_activity = has_activity() + + # Get progress info + progress_percent = '' + progress = getattr(window, 'progress', None) + if progress is not None and progress.state is not ProgressState.unset: + if progress.state is ProgressState.indeterminate: + progress_percent = '[…] ' + elif progress.percent > 0: + progress_percent = f'[{progress.percent}%] ' + + data = WindowTitleData( title=window.title or '', is_active=is_active, window_id=wid, tab_id=self.tab_id, + needs_attention=needs_attention, + has_activity_since_last_focus=has_activity, ) - pts.render(data) + pts.render(data, progress_percent) - set_pane_title_bar_render_data( + set_window_title_bar_render_data( self.os_window_id, self.tab_id, wid, pts.screen, title_geom.left, title_geom.top, title_geom.right, title_geom.bottom, ) diff --git a/tools/cmd/at/set_colors.go b/tools/cmd/at/set_colors.go index 6557cfe1d..6370ada3b 100644 --- a/tools/cmd/at/set_colors.go +++ b/tools/cmd/at/set_colors.go @@ -15,15 +15,19 @@ import ( var nullable_colors = map[string]bool{ // generated by gen-config.py do not edit // NULLABLE_COLORS_START - "active_border_color": true, - "cursor": true, - "cursor_text_color": true, - "cursor_trail_color": true, - "selection_background": true, - "selection_foreground": true, - "tab_bar_background": true, - "tab_bar_margin_color": true, - "visual_bell_color": true, + "active_border_color": true, + "cursor": true, + "cursor_text_color": true, + "cursor_trail_color": true, + "selection_background": true, + "selection_foreground": true, + "tab_bar_background": true, + "tab_bar_margin_color": true, + "visual_bell_color": true, + "window_title_bar_active_background": true, + "window_title_bar_active_foreground": true, + "window_title_bar_inactive_background": true, + "window_title_bar_inactive_foreground": true, // NULLABLE_COLORS_END } diff --git a/tools/themes/collection.go b/tools/themes/collection.go index 5ba3fb4d1..355c6d77f 100644 --- a/tools/themes/collection.go +++ b/tools/themes/collection.go @@ -35,294 +35,294 @@ var _ = fmt.Print var AllColorSettingNames = map[string]bool{ // {{{ // generated by gen-config.py do not edit // ALL_COLORS_START - "active_border_color": true, - "active_tab_background": true, - "active_tab_foreground": true, - "background": true, - "bell_border_color": true, - "color0": true, - "color1": true, - "color10": true, - "color100": true, - "color101": true, - "color102": true, - "color103": true, - "color104": true, - "color105": true, - "color106": true, - "color107": true, - "color108": true, - "color109": true, - "color11": true, - "color110": true, - "color111": true, - "color112": true, - "color113": true, - "color114": true, - "color115": true, - "color116": true, - "color117": true, - "color118": true, - "color119": true, - "color12": true, - "color120": true, - "color121": true, - "color122": true, - "color123": true, - "color124": true, - "color125": true, - "color126": true, - "color127": true, - "color128": true, - "color129": true, - "color13": true, - "color130": true, - "color131": true, - "color132": true, - "color133": true, - "color134": true, - "color135": true, - "color136": true, - "color137": true, - "color138": true, - "color139": true, - "color14": true, - "color140": true, - "color141": true, - "color142": true, - "color143": true, - "color144": true, - "color145": true, - "color146": true, - "color147": true, - "color148": true, - "color149": true, - "color15": true, - "color150": true, - "color151": true, - "color152": true, - "color153": true, - "color154": true, - "color155": true, - "color156": true, - "color157": true, - "color158": true, - "color159": true, - "color16": true, - "color160": true, - "color161": true, - "color162": true, - "color163": true, - "color164": true, - "color165": true, - "color166": true, - "color167": true, - "color168": true, - "color169": true, - "color17": true, - "color170": true, - "color171": true, - "color172": true, - "color173": true, - "color174": true, - "color175": true, - "color176": true, - "color177": true, - "color178": true, - "color179": true, - "color18": true, - "color180": true, - "color181": true, - "color182": true, - "color183": true, - "color184": true, - "color185": true, - "color186": true, - "color187": true, - "color188": true, - "color189": true, - "color19": true, - "color190": true, - "color191": true, - "color192": true, - "color193": true, - "color194": true, - "color195": true, - "color196": true, - "color197": true, - "color198": true, - "color199": true, - "color2": true, - "color20": true, - "color200": true, - "color201": true, - "color202": true, - "color203": true, - "color204": true, - "color205": true, - "color206": true, - "color207": true, - "color208": true, - "color209": true, - "color21": true, - "color210": true, - "color211": true, - "color212": true, - "color213": true, - "color214": true, - "color215": true, - "color216": true, - "color217": true, - "color218": true, - "color219": true, - "color22": true, - "color220": true, - "color221": true, - "color222": true, - "color223": true, - "color224": true, - "color225": true, - "color226": true, - "color227": true, - "color228": true, - "color229": true, - "color23": true, - "color230": true, - "color231": true, - "color232": true, - "color233": true, - "color234": true, - "color235": true, - "color236": true, - "color237": true, - "color238": true, - "color239": true, - "color24": true, - "color240": true, - "color241": true, - "color242": true, - "color243": true, - "color244": true, - "color245": true, - "color246": true, - "color247": true, - "color248": true, - "color249": true, - "color25": true, - "color250": true, - "color251": true, - "color252": true, - "color253": true, - "color254": true, - "color255": true, - "color26": true, - "color27": true, - "color28": true, - "color29": true, - "color3": true, - "color30": true, - "color31": true, - "color32": true, - "color33": true, - "color34": true, - "color35": true, - "color36": true, - "color37": true, - "color38": true, - "color39": true, - "color4": true, - "color40": true, - "color41": true, - "color42": true, - "color43": true, - "color44": true, - "color45": true, - "color46": true, - "color47": true, - "color48": true, - "color49": true, - "color5": true, - "color50": true, - "color51": true, - "color52": true, - "color53": true, - "color54": true, - "color55": true, - "color56": true, - "color57": true, - "color58": true, - "color59": true, - "color6": true, - "color60": true, - "color61": true, - "color62": true, - "color63": true, - "color64": true, - "color65": true, - "color66": true, - "color67": true, - "color68": true, - "color69": true, - "color7": true, - "color70": true, - "color71": true, - "color72": true, - "color73": true, - "color74": true, - "color75": true, - "color76": true, - "color77": true, - "color78": true, - "color79": true, - "color8": true, - "color80": true, - "color81": true, - "color82": true, - "color83": true, - "color84": true, - "color85": true, - "color86": true, - "color87": true, - "color88": true, - "color89": true, - "color9": true, - "color90": true, - "color91": true, - "color92": true, - "color93": true, - "color94": true, - "color95": true, - "color96": true, - "color97": true, - "color98": true, - "color99": true, - "cursor": true, - "cursor_text_color": true, - "cursor_trail_color": true, - "foreground": true, - "inactive_border_color": true, - "inactive_tab_background": true, - "inactive_tab_foreground": true, - "macos_titlebar_color": true, - "mark1_background": true, - "mark1_foreground": true, - "mark2_background": true, - "mark2_foreground": true, - "mark3_background": true, - "mark3_foreground": true, - "pane_title_bar_active_bg": true, - "pane_title_bar_active_fg": true, - "pane_title_bar_inactive_bg": true, - "pane_title_bar_inactive_fg": true, - "scrollbar_handle_color": true, - "scrollbar_track_color": true, - "selection_background": true, - "selection_foreground": true, - "tab_bar_background": true, - "tab_bar_margin_color": true, - "url_color": true, - "visual_bell_color": true, - "wayland_titlebar_color": true, // ALL_COLORS_END + "active_border_color": true, + "active_tab_background": true, + "active_tab_foreground": true, + "background": true, + "bell_border_color": true, + "color0": true, + "color1": true, + "color10": true, + "color100": true, + "color101": true, + "color102": true, + "color103": true, + "color104": true, + "color105": true, + "color106": true, + "color107": true, + "color108": true, + "color109": true, + "color11": true, + "color110": true, + "color111": true, + "color112": true, + "color113": true, + "color114": true, + "color115": true, + "color116": true, + "color117": true, + "color118": true, + "color119": true, + "color12": true, + "color120": true, + "color121": true, + "color122": true, + "color123": true, + "color124": true, + "color125": true, + "color126": true, + "color127": true, + "color128": true, + "color129": true, + "color13": true, + "color130": true, + "color131": true, + "color132": true, + "color133": true, + "color134": true, + "color135": true, + "color136": true, + "color137": true, + "color138": true, + "color139": true, + "color14": true, + "color140": true, + "color141": true, + "color142": true, + "color143": true, + "color144": true, + "color145": true, + "color146": true, + "color147": true, + "color148": true, + "color149": true, + "color15": true, + "color150": true, + "color151": true, + "color152": true, + "color153": true, + "color154": true, + "color155": true, + "color156": true, + "color157": true, + "color158": true, + "color159": true, + "color16": true, + "color160": true, + "color161": true, + "color162": true, + "color163": true, + "color164": true, + "color165": true, + "color166": true, + "color167": true, + "color168": true, + "color169": true, + "color17": true, + "color170": true, + "color171": true, + "color172": true, + "color173": true, + "color174": true, + "color175": true, + "color176": true, + "color177": true, + "color178": true, + "color179": true, + "color18": true, + "color180": true, + "color181": true, + "color182": true, + "color183": true, + "color184": true, + "color185": true, + "color186": true, + "color187": true, + "color188": true, + "color189": true, + "color19": true, + "color190": true, + "color191": true, + "color192": true, + "color193": true, + "color194": true, + "color195": true, + "color196": true, + "color197": true, + "color198": true, + "color199": true, + "color2": true, + "color20": true, + "color200": true, + "color201": true, + "color202": true, + "color203": true, + "color204": true, + "color205": true, + "color206": true, + "color207": true, + "color208": true, + "color209": true, + "color21": true, + "color210": true, + "color211": true, + "color212": true, + "color213": true, + "color214": true, + "color215": true, + "color216": true, + "color217": true, + "color218": true, + "color219": true, + "color22": true, + "color220": true, + "color221": true, + "color222": true, + "color223": true, + "color224": true, + "color225": true, + "color226": true, + "color227": true, + "color228": true, + "color229": true, + "color23": true, + "color230": true, + "color231": true, + "color232": true, + "color233": true, + "color234": true, + "color235": true, + "color236": true, + "color237": true, + "color238": true, + "color239": true, + "color24": true, + "color240": true, + "color241": true, + "color242": true, + "color243": true, + "color244": true, + "color245": true, + "color246": true, + "color247": true, + "color248": true, + "color249": true, + "color25": true, + "color250": true, + "color251": true, + "color252": true, + "color253": true, + "color254": true, + "color255": true, + "color26": true, + "color27": true, + "color28": true, + "color29": true, + "color3": true, + "color30": true, + "color31": true, + "color32": true, + "color33": true, + "color34": true, + "color35": true, + "color36": true, + "color37": true, + "color38": true, + "color39": true, + "color4": true, + "color40": true, + "color41": true, + "color42": true, + "color43": true, + "color44": true, + "color45": true, + "color46": true, + "color47": true, + "color48": true, + "color49": true, + "color5": true, + "color50": true, + "color51": true, + "color52": true, + "color53": true, + "color54": true, + "color55": true, + "color56": true, + "color57": true, + "color58": true, + "color59": true, + "color6": true, + "color60": true, + "color61": true, + "color62": true, + "color63": true, + "color64": true, + "color65": true, + "color66": true, + "color67": true, + "color68": true, + "color69": true, + "color7": true, + "color70": true, + "color71": true, + "color72": true, + "color73": true, + "color74": true, + "color75": true, + "color76": true, + "color77": true, + "color78": true, + "color79": true, + "color8": true, + "color80": true, + "color81": true, + "color82": true, + "color83": true, + "color84": true, + "color85": true, + "color86": true, + "color87": true, + "color88": true, + "color89": true, + "color9": true, + "color90": true, + "color91": true, + "color92": true, + "color93": true, + "color94": true, + "color95": true, + "color96": true, + "color97": true, + "color98": true, + "color99": true, + "cursor": true, + "cursor_text_color": true, + "cursor_trail_color": true, + "foreground": true, + "inactive_border_color": true, + "inactive_tab_background": true, + "inactive_tab_foreground": true, + "macos_titlebar_color": true, + "mark1_background": true, + "mark1_foreground": true, + "mark2_background": true, + "mark2_foreground": true, + "mark3_background": true, + "mark3_foreground": true, + "scrollbar_handle_color": true, + "scrollbar_track_color": true, + "selection_background": true, + "selection_foreground": true, + "tab_bar_background": true, + "tab_bar_margin_color": true, + "url_color": true, + "visual_bell_color": true, + "wayland_titlebar_color": true, + "window_title_bar_active_background": true, + "window_title_bar_active_foreground": true, + "window_title_bar_inactive_background": true, + "window_title_bar_inactive_foreground": true, // ALL_COLORS_END } // }}} type JSONMetadata struct { From c68f712d19008c9d034a5d79253413cc538bd969 Mon Sep 17 00:00:00 2001 From: mcrmck Date: Mon, 2 Mar 2026 00:10:37 -0500 Subject: [PATCH 3/5] Fix CI: sort imports and fix Sphinx cross-references Co-Authored-By: Claude Opus 4.6 --- kitty/options/definition.py | 8 ++++---- kitty/tabs.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 6cfed2f66..66ca814d6 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1501,7 +1501,7 @@ opt('window_title_bar_active_foreground', 'none', option_type='to_color_or_none', ctype='color_or_none_as_int', long_text=''' Foreground color for the active window title bar. Defaults to -:opt:`active_tab_foreground` when set to :code:`none`. +the corresponding tab bar color (:code:`active_tab_foreground`) when set to :code:`none`. ''' ) @@ -1509,7 +1509,7 @@ opt('window_title_bar_active_background', 'none', option_type='to_color_or_none', ctype='color_or_none_as_int', long_text=''' Background color for the active window title bar. Defaults to -:opt:`active_tab_background` when set to :code:`none`. +the corresponding tab bar color (:code:`active_tab_background`) when set to :code:`none`. ''' ) @@ -1517,7 +1517,7 @@ opt('window_title_bar_inactive_foreground', 'none', option_type='to_color_or_none', ctype='color_or_none_as_int', long_text=''' Foreground color for inactive window title bars. Defaults to -:opt:`inactive_tab_foreground` when set to :code:`none`. +the corresponding tab bar color (:code:`inactive_tab_foreground`) when set to :code:`none`. ''' ) @@ -1525,7 +1525,7 @@ opt('window_title_bar_inactive_background', 'none', option_type='to_color_or_none', ctype='color_or_none_as_int', long_text=''' Background color for inactive window title bars. Defaults to -:opt:`inactive_tab_background` when set to :code:`none`. +the corresponding tab bar color (:code:`inactive_tab_background`) when set to :code:`none`. ''' ) diff --git a/kitty/tabs.py b/kitty/tabs.py index 456f38a17..119a9e7a4 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -15,7 +15,6 @@ from gettext import gettext as _ from typing import Any, Concatenate, Deque, NamedTuple, Optional, ParamSpec, TypeVar, cast from .borders import Border, Borders -from .window_title_bar import WindowTitleBarManager from .child import Child from .cli_stub import CLIOptions, SaveAsSessionOptions from .constants import appname @@ -62,6 +61,7 @@ from .typing_compat import EdgeLiteral, SessionTab, SessionType, TypedDict from .utils import cmdline_for_hold, color_as_int, log_error, platform_window_id, resolved_shell, shlex_split, which from .window import CwdRequest, Watchers, Window, WindowCreationSpec, WindowDict, global_watchers from .window_list import WindowList +from .window_title_bar import WindowTitleBarManager P = ParamSpec('P') T = TypeVar('T') From dd26469cb390760feb477ca7ad80d5a7c042abc5 Mon Sep 17 00:00:00 2001 From: mcrmck Date: Wed, 4 Mar 2026 20:11:53 -0500 Subject: [PATCH 4/5] Rework window title bar architecture per review feedback - Eliminate double set_geometry() call: removed _apply_window_title_bars() which post-processed geometry causing expensive SIGWINCH to children - Move title bar screen ownership to Window objects instead of central manager, with show_title_bar flag set during layout before do_layout() - Window.set_geometry() now handles title bar geometry internally: self.geometry stays at layout-computed value (borders/padding correct), only C-side render data diverges via adjusted top/bottom - Hide title bar for 1-row windows (ynum <= 1) - Hide title bar when template evaluates to empty/whitespace - Optimize C render loop: merge title bar GPU prep and draw into existing per-window loops, use trd pointer and is_visible=false, use num_visible_windows > 1 guard. Eliminates separate iteration passes. - Simplify WindowTitleBarManager to thin coordinator Note on C-side GPU prep placement: the suggested patch placed send_cell_data_to_gpu for title bars inside the is_active_window branch only. This caused a segfault (NULL deref in gleRunVertexSubmitImmediate) because inactive windows' title bars had valid screen/geometry but no GPU data uploaded, yet draw_cells was called for all visible title bars. Moved to the per-window visibility block alongside the main window's send_cell_data_to_gpu call so all visible title bars get GPU data prepared. The draw loop matches the suggested patch exactly. Co-Authored-By: Claude Opus 4.6 --- kitty/child-monitor.c | 25 +++----- kitty/layout/base.py | 24 ++------ kitty/window.py | 98 +++++++++++++++++++++++++++++-- kitty/window_title_bar.py | 118 +++----------------------------------- 4 files changed, 116 insertions(+), 149 deletions(-) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index d1afce3bd..632375fde 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -781,7 +781,6 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * set_maximum_wait(OPT(cursor_trail) - now + WD.screen->cursor->position_changed_by_client_at); } } - } else { if (WD.screen->cursor_render_info.render_even_when_unfocused) { if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true; @@ -806,12 +805,12 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * } if (send_cell_data_to_gpu(WD.vao_idx, WD.screen, os_window)) needs_render = true; if (WD.screen->start_visual_bell_at != 0) needs_render = true; - } - // Prepare window title bar screen data for GPU - if (w->visible && w->window_title_render_data.screen) { - CursorRenderInfo *cri = &w->window_title_render_data.screen->cursor_render_info; - zero_at_ptr(cri); - if (send_cell_data_to_gpu(w->window_title_render_data.vao_idx, w->window_title_render_data.screen, os_window)) needs_render = true; + // Prepare window title bar screen data for GPU + WindowRenderData *trd = &w->window_title_render_data; + if (trd->screen) { + trd->screen->cursor_render_info.is_visible = false; + if (send_cell_data_to_gpu(trd->vao_idx, trd->screen, os_window)) needs_render = true; + } } } return needs_render || was_previously_rendered_with_layers != os_window->needs_layers; @@ -872,15 +871,9 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co if (is_active_window) active_window = w; draw_cells(&WD, os_window, is_active_window, false, num_of_visible_windows == 1, w); if (WD.screen->start_visual_bell_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); - } - } - // Draw window title bars - for (unsigned int i = 0; i < tab->num_windows; i++) { - Window *w = tab->windows + i; - if (w->visible && w->window_title_render_data.screen && - w->window_title_render_data.geometry.right > w->window_title_render_data.geometry.left && - w->window_title_render_data.geometry.bottom > w->window_title_render_data.geometry.top) { - draw_cells(&w->window_title_render_data, os_window, i == tab->active_window, true, false, NULL); + WindowRenderData *trd = &w->window_title_render_data; + if (trd->screen && num_visible_windows > 1 && trd->geometry.right > trd->geometry.left && trd->geometry.bottom > trd->geometry.top) + draw_cells(trd, os_window, i == tab->active_window, true, false, NULL); } } setup_os_window_for_rendering(os_window, tab, active_window, false); diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 3c7f3a72a..3d7abdcc7 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -367,27 +367,15 @@ class Layout: self._set_dimensions(all_windows) self.update_visibility(all_windows) self.blank_rects = [] - self.do_layout(all_windows) - self._apply_window_title_bars(all_windows) - - def _apply_window_title_bars(self, all_windows: WindowList) -> None: + # Set show_title_bar flag on each visible window before layout opts = get_options() - position = opts.window_title_bar - if position == 'none': - return + title_bar_enabled = opts.window_title_bar != 'none' visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) - if len(visible_groups) < 2: - return - ch = lgd.cell_height + num_visible = len(visible_groups) for wg in visible_groups: - geom = wg.geometry - if geom is None: - continue - if position == 'top': - new_geom = geom._replace(top=geom.top + ch, ynum=max(1, geom.ynum - 1)) - else: - new_geom = geom._replace(bottom=geom.bottom - ch, ynum=max(1, geom.ynum - 1)) - wg.set_geometry(new_geom) + for w in wg.windows: + w.show_title_bar = title_bar_enabled and num_visible > 1 + self.do_layout(all_windows) def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None: bw = 1 if self.must_draw_borders else 0 diff --git a/kitty/window.py b/kitty/window.py index d1ba36eb3..4ef2c8130 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -84,6 +84,7 @@ from .fast_data_types import ( set_window_logo, set_window_padding, set_window_render_data, + set_window_title_bar_render_data, update_ime_position_for_window, update_pointer_shape, update_window_title, @@ -732,6 +733,8 @@ class Window: self.tabref: Callable[[], TabType | None] = weakref.ref(tab) self.destroyed = False self.geometry: WindowGeometry = WindowGeometry(0, 0, 0, 0, 0, 0) + self.show_title_bar: bool = False + self._title_bar_screen: Any = None self.needs_layout = True self.is_visible_in_layout: bool = True self.child = child @@ -977,13 +980,36 @@ class Window: def set_geometry(self, new_geometry: WindowGeometry) -> None: if self.destroyed: return - if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines: - self.screen.resize(max(0, new_geometry.ynum), max(0, new_geometry.xnum)) + # Determine if we need a title bar and compute adjusted dimensions + opts = get_options() + position = opts.window_title_bar + show_tb = self.show_title_bar and new_geometry.ynum > 1 + + if show_tb: + render_ynum = new_geometry.ynum - 1 + cell_width, cell_height = cell_size_for_window(self.os_window_id) + if position == 'top': + render_top = new_geometry.top + cell_height + render_bottom = new_geometry.bottom + tb_top = new_geometry.top + tb_bottom = new_geometry.top + cell_height + else: + render_top = new_geometry.top + render_bottom = new_geometry.bottom - cell_height + tb_top = new_geometry.bottom - cell_height + tb_bottom = new_geometry.bottom + else: + render_ynum = new_geometry.ynum + render_top = new_geometry.top + render_bottom = new_geometry.bottom + + if self.needs_layout or new_geometry.xnum != self.screen.columns or render_ynum != self.screen.lines: + self.screen.resize(max(0, render_ynum), max(0, new_geometry.xnum)) self.needs_layout = False call_watchers(weakref.ref(self), 'on_resize', {'old_geometry': self.geometry, 'new_geometry': new_geometry}) current_pty_size = ( self.screen.lines, self.screen.columns, - max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top)) + max(0, new_geometry.right - new_geometry.left), max(0, render_bottom - render_top)) update_ime_position = False if current_pty_size != self.last_reported_pty_size: if self._pause_resize_notifications_to_child is None: @@ -993,14 +1019,78 @@ class Window: else: mark_os_window_dirty(self.os_window_id) + # Store original geometry for borders/padding calculations self.geometry = g = new_geometry + # Set C-side render data with adjusted top/bottom for content area set_window_render_data(self.os_window_id, self.tab_id, self.id, self.screen, - g.left, g.top, g.right, g.bottom, + g.left, render_top, g.right, render_bottom, g.spaces.left, g.spaces.top, g.spaces.right, g.spaces.bottom) self.update_effective_padding() + + # Handle title bar screen + if show_tb: + from .window_title_bar import WindowTitleBarScreen + if self._title_bar_screen is None: + self._title_bar_screen = WindowTitleBarScreen(self.os_window_id, cell_width, cell_height) + tb_geom = WindowGeometry( + left=g.left, top=tb_top, right=g.right, bottom=tb_bottom, + xnum=0, ynum=1, + ) + self._title_bar_screen.layout(tb_geom) + set_window_title_bar_render_data( + self.os_window_id, self.tab_id, self.id, self._title_bar_screen.screen, + tb_geom.left, tb_geom.top, tb_geom.right, tb_geom.bottom, + ) + elif self._title_bar_screen is not None: + # Clear title bar render data + set_window_title_bar_render_data( + self.os_window_id, self.tab_id, self.id, self._title_bar_screen.screen, + 0, 0, 0, 0, + ) + self._title_bar_screen = None + if update_ime_position: update_ime_position_for_window(self.id, True) + def update_title_bar(self, is_active: bool = False) -> None: + pts = self._title_bar_screen + if pts is None: + return + from .progress import ProgressState + from .window_title_bar import WindowTitleData + + progress_percent = '' + if self.progress.state is not ProgressState.unset: + if self.progress.state is ProgressState.indeterminate: + progress_percent = '[…] ' + elif self.progress.percent > 0: + progress_percent = f'[{self.progress.percent}%] ' + + has_activity = self.has_activity_since_last_focus + + data = WindowTitleData( + title=self.title or '', + is_active=is_active, + window_id=self.id, + tab_id=self.tab_id, + needs_attention=self.needs_attention, + has_activity_since_last_focus=has_activity, + ) + rendered_title = pts.render(data, progress_percent) + + # If template evaluates to empty/whitespace, zero title bar geometry to hide it + if not rendered_title or not rendered_title.strip(): + set_window_title_bar_render_data( + self.os_window_id, self.tab_id, self.id, pts.screen, + 0, 0, 0, 0, + ) + else: + g = pts.geometry + set_window_title_bar_render_data( + self.os_window_id, self.tab_id, self.id, pts.screen, + g.left, g.top, g.right, g.bottom, + ) + def close(self) -> None: get_boss().mark_window_for_close(self) diff --git a/kitty/window_title_bar.py b/kitty/window_title_bar.py index fcc53a4bb..921e7a35a 100644 --- a/kitty/window_title_bar.py +++ b/kitty/window_title_bar.py @@ -9,11 +9,8 @@ from .constants import config_dir from .fast_data_types import ( DECAWM, Screen, - cell_size_for_window, get_options, - set_window_title_bar_render_data, ) -from .progress import ProgressState from .rgb import color_as_sgr, color_from_int, to_color from .types import WindowGeometry, run_once from .utils import color_as_int, log_error, sgr_sanitizer_pat @@ -150,7 +147,7 @@ class WindowTitleBarScreen: self.screen.resize(1, ncells) self.geometry = geometry - def render(self, data: WindowTitleData, progress_percent: str) -> None: + def render(self, data: WindowTitleData, progress_percent: str) -> str: opts = get_options() s = self.screen s.cursor.x = 0 @@ -226,122 +223,21 @@ class WindowTitleBarScreen: while s.cursor.x < s.columns: s.draw(' ') + return title_str + class WindowTitleBarManager: def __init__(self, os_window_id: int, tab_id: int): self.os_window_id = os_window_id self.tab_id = tab_id - self._screens: dict[int, WindowTitleBarScreen] = {} - - def _clear_all(self) -> None: - for wid, pts in self._screens.items(): - # Zero geometry so the C render loop skips drawing - set_window_title_bar_render_data( - self.os_window_id, self.tab_id, wid, pts.screen, - 0, 0, 0, 0, - ) - self._screens.clear() def update(self, all_windows: WindowList) -> None: - opts = get_options() - position = opts.window_title_bar - if position == 'none': - if self._screens: - self._clear_all() - return - - visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) - if len(visible_groups) < 2: - if self._screens: - self._clear_all() - return - - cell_width, cell_height = cell_size_for_window(self.os_window_id) active_group = all_windows.active_group - seen_window_ids: set[int] = set() - - for wg in visible_groups: - geom = wg.geometry - if geom is None: - continue - - window = wg.windows[-1] if wg.windows else None - if window is None: - continue - - # Validate geometry has enough space for a title bar - if geom.right <= geom.left or geom.bottom <= geom.top: - continue - if position == 'top' and geom.top < cell_height: - continue - if position == 'bottom' and geom.bottom + cell_height < geom.bottom: # overflow check - continue - - wid = window.id - seen_window_ids.add(wid) - - if wid not in self._screens: - self._screens[wid] = WindowTitleBarScreen(self.os_window_id, cell_width, cell_height) - - pts = self._screens[wid] - - # Calculate title bar geometry - if position == 'top': - title_geom = WindowGeometry( - left=geom.left, - top=geom.top - cell_height, - right=geom.right, - bottom=geom.top, - xnum=0, ynum=1, - ) - else: - title_geom = WindowGeometry( - left=geom.left, - top=geom.bottom, - right=geom.right, - bottom=geom.bottom + cell_height, - xnum=0, ynum=1, - ) - - pts.layout(title_geom) - + for wg in all_windows.iter_all_layoutable_groups(only_visible=True): is_active = wg is active_group - - # Get bell/activity state from the window object - needs_attention = getattr(window, 'needs_attention', False) - has_activity = getattr(window, 'has_activity_since_last_focus', False) - if callable(has_activity): - has_activity = has_activity() - - # Get progress info - progress_percent = '' - progress = getattr(window, 'progress', None) - if progress is not None and progress.state is not ProgressState.unset: - if progress.state is ProgressState.indeterminate: - progress_percent = '[…] ' - elif progress.percent > 0: - progress_percent = f'[{progress.percent}%] ' - - data = WindowTitleData( - title=window.title or '', - is_active=is_active, - window_id=wid, - tab_id=self.tab_id, - needs_attention=needs_attention, - has_activity_since_last_focus=has_activity, - ) - pts.render(data, progress_percent) - - set_window_title_bar_render_data( - self.os_window_id, self.tab_id, wid, pts.screen, - title_geom.left, title_geom.top, title_geom.right, title_geom.bottom, - ) - - # Clean up screens for windows that are no longer visible - stale = set(self._screens) - seen_window_ids - for wid in stale: - del self._screens[wid] + for w in wg.windows: + w.update_title_bar(is_active=is_active) def destroy(self) -> None: - self._screens.clear() + pass From f2ae5d00288cd0b5ab2789fc8b14d546da7bb8c4 Mon Sep 17 00:00:00 2001 From: mcrmck Date: Wed, 4 Mar 2026 21:14:54 -0500 Subject: [PATCH 5/5] Add window_title_bar_min_windows option, simplify window_title_bar - Add window_title_bar_min_windows (0=never, 1=always, 2+=threshold) similar to tab_bar_min_tabs, to control when title bars appear - Remove 'none' choice from window_title_bar so it purely controls position (top/bottom); disabling is now via min_windows 0 - Only hide title bar for truly empty template strings, not whitespace-only, so users can have intentionally blank bars Co-Authored-By: Claude Opus 4.6 --- kitty/layout/base.py | 4 ++-- kitty/options/definition.py | 20 ++++++++++++++------ kitty/options/parse.py | 5 ++++- kitty/options/types.py | 6 ++++-- kitty/window.py | 4 ++-- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 3d7abdcc7..91d3c6761 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -369,12 +369,12 @@ class Layout: self.blank_rects = [] # Set show_title_bar flag on each visible window before layout opts = get_options() - title_bar_enabled = opts.window_title_bar != 'none' + min_windows = opts.window_title_bar_min_windows visible_groups = list(all_windows.iter_all_layoutable_groups(only_visible=True)) num_visible = len(visible_groups) for wg in visible_groups: for w in wg.windows: - w.show_title_bar = title_bar_enabled and num_visible > 1 + w.show_title_bar = min_windows > 0 and num_visible >= min_windows self.do_layout(all_windows) def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None: diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 66ca814d6..780abf9e7 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1467,16 +1467,24 @@ actually resize the window itself, but instead, the layout row/column/slot, whic in multiple windows getting resized. ''') -opt('window_title_bar', 'none', - choices=('none', 'top', 'bottom'), +opt('window_title_bar', 'top', + choices=('top', 'bottom'), long_text=''' -Show a title bar for each window when there are multiple windows in a tab. -The title bar displays the window title and is hidden when only a single window -is visible. The value controls the position of the title bar relative to the -window content. Set to :code:`none` to disable. +Control the position of the window title bar relative to the window content. +Use :opt:`window_title_bar_min_windows` to control when title bars are shown. ''' ) +opt('window_title_bar_min_windows', '0', + option_type='positive_int', + long_text=''' +The minimum number of visible windows in a tab before window title bars +are shown. A value of :code:`0` means never show. :code:`1` means always +show. :code:`2` or more means show only when at least that many windows +are visible. Similar to :opt:`tab_bar_min_tabs` for the tab bar. +''' +) + opt('window_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.window}{progress_percent}{title}"', option_type='tab_title_template', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 2bf282acb..37c1c7572 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1513,7 +1513,10 @@ class Parser: raise ValueError(f"The value {val} is not a valid choice for window_title_bar") ans["window_title_bar"] = val - choices_for_window_title_bar = frozenset(('none', 'top', 'bottom')) + choices_for_window_title_bar = frozenset(('top', 'bottom')) + + def window_title_bar_min_windows(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_title_bar_min_windows'] = positive_int(val) def window_title_bar_active_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_title_bar_active_background'] = to_color_or_none(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 8cf5bb2d6..81e3d2b94 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -37,7 +37,7 @@ choices_for_terminfo_type = typing.Literal['path', 'direct', 'none'] choices_for_undercurl_style = typing.Literal['thin-sparse', 'thin-dense', 'thick-sparse', 'thick-dense'] choices_for_underline_hyperlinks = typing.Literal['hover', 'always', 'never'] choices_for_window_logo_position = choices_for_placement_strategy -choices_for_window_title_bar = typing.Literal['none', 'top', 'bottom'] +choices_for_window_title_bar = typing.Literal['top', 'bottom'] choices_for_window_title_bar_align = choices_for_tab_bar_align option_names = ( @@ -501,6 +501,7 @@ option_names = ( 'window_resize_step_cells', 'window_resize_step_lines', 'window_title_bar', + 'window_title_bar_min_windows', 'window_title_bar_active_background', 'window_title_bar_active_foreground', 'window_title_bar_align', @@ -700,7 +701,8 @@ class Options: window_padding_width: FloatEdges = FloatEdges(left=0, top=0, right=0, bottom=0) window_resize_step_cells: int = 2 window_resize_step_lines: int = 2 - window_title_bar: choices_for_window_title_bar = 'none' + window_title_bar: choices_for_window_title_bar = 'top' + window_title_bar_min_windows: int = 0 window_title_bar_active_background: kitty.fast_data_types.Color | None = None window_title_bar_active_foreground: kitty.fast_data_types.Color | None = None window_title_bar_align: choices_for_window_title_bar_align = 'center' diff --git a/kitty/window.py b/kitty/window.py index 4ef2c8130..ddc257d4c 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1078,8 +1078,8 @@ class Window: ) rendered_title = pts.render(data, progress_percent) - # If template evaluates to empty/whitespace, zero title bar geometry to hide it - if not rendered_title or not rendered_title.strip(): + # If template evaluates to empty string, zero title bar geometry to hide it + if not rendered_title: set_window_title_bar_render_data( self.os_window_id, self.tab_id, self.id, pts.screen, 0, 0, 0, 0,