diff --git a/docs/changelog.rst b/docs/changelog.rst index a036d5338..03dec49f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ To update |kitty|, :doc:`follow the instructions `. 0.23.2 [future] ---------------------- +- A new option :option:`tab_bar_align` to draw the tab bar centered or right + aligned (:iss:`3946`) + - Allow the user to supply a custom Python function to draw tab bar. See :option:`tab_bar_style` diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 1274d93d4..9cdcca3f1 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1058,6 +1058,9 @@ class Screen: def has_activity_since_last_focus(self) -> bool: pass + def insert_characters(self, num: int) -> None: + pass + def set_tab_bar_render_data( os_window_id: int, xstart: float, ystart: float, dx: float, dy: float, diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 3cccdc4a3..d7e1473d5 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -907,6 +907,8 @@ The tab bar style, can be one of: ''' ) +opt('tab_bar_align', 'left', choices=('left', 'center', 'right'), long_text='The horizontal alignment of the tab bar') + opt('tab_bar_min_tabs', '2', option_type='tab_bar_min_tabs', ctype='uint', long_text='The minimum number of tabs that must exist before the tab bar is shown' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 43e6ab594..63cd7f01e 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1152,6 +1152,14 @@ class Parser: def tab_activity_symbol(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['tab_activity_symbol'] = tab_activity_symbol(val) + def tab_bar_align(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + val = val.lower() + if val not in self.choices_for_tab_bar_align: + 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')) + def tab_bar_background(self, val: str, ans: typing.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 8a66992b3..41f77fa1a 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -23,6 +23,7 @@ if typing.TYPE_CHECKING: choices_for_pointer_shape_when_dragging = typing.Literal['arrow', 'beam', 'hand'] choices_for_pointer_shape_when_grabbed = typing.Literal['arrow', 'beam', 'hand'] choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart'] + 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'] @@ -35,6 +36,7 @@ else: choices_for_pointer_shape_when_dragging = str choices_for_pointer_shape_when_grabbed = str choices_for_strip_trailing_spaces = str + choices_for_tab_bar_align = str choices_for_tab_bar_style = str choices_for_tab_powerline_style = str choices_for_tab_switch_strategy = str @@ -404,6 +406,7 @@ option_names = ( # {{{ 'symbol_map', 'sync_to_monitor', 'tab_activity_symbol', + 'tab_bar_align', 'tab_bar_background', 'tab_bar_edge', 'tab_bar_margin_height', @@ -536,6 +539,7 @@ class Options: strip_trailing_spaces: choices_for_strip_trailing_spaces = 'never' sync_to_monitor: bool = True tab_activity_symbol: typing.Optional[str] = None + tab_bar_align: choices_for_tab_bar_align = 'left' tab_bar_background: typing.Optional[kitty.rgb.Color] = None tab_bar_edge: int = 3 tab_bar_margin_height: TabBarMarginHeight = TabBarMarginHeight(outer=0, inner=0) diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index a0e9a2698..f64a0e581 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -3,7 +3,7 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import os -from functools import lru_cache, wraps +from functools import lru_cache, partial, wraps from typing import ( Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple ) @@ -415,6 +415,12 @@ class TabBar: self.draw_func = load_custom_draw_tab() else: self.draw_func = draw_tab_with_fade + if opts.tab_bar_align == 'center': + self.align: Callable[[], None] = partial(self.align_with_factor, 2) + elif opts.tab_bar_align == 'right': + self.align = self.align_with_factor + else: + self.align = lambda: None def patch_colors(self, spec: Dict[str, Any]) -> None: if 'active_tab_foreground' in spec: @@ -497,6 +503,17 @@ class TabBar: break s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab self.cell_ranges = cr + self.align() + + def align_with_factor(self, factor: int = 1) -> None: + if not self.cell_ranges: + return + end = self.cell_ranges[-1][1] + if end < self.screen.columns - 1: + shift = (self.screen.columns - end) // factor + self.screen.cursor.x = 0 + self.screen.insert_characters(shift) + self.cell_ranges = [(s + shift, e + shift) for (s, e) in self.cell_ranges] def destroy(self) -> None: self.screen.reset_callbacks()