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 <noreply@anthropic.com>
This commit is contained in:
mcrmck
2026-02-01 00:37:40 -05:00
parent 3281a8d634
commit ab3a8ca56a
11 changed files with 731 additions and 286 deletions

View File

@@ -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() # }}}

36
kitty/options/parse.py generated
View File

@@ -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)

20
kitty/options/types.py generated
View File

@@ -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'